mirror of https://github.com/AMT-Cheif/drift.git
Merge branch 'develop' into views-triggers-indexes
# Conflicts: # moor/lib/moor.dart # moor/lib/src/runtime/api/query_engine.dart # moor/lib/src/runtime/query_builder/schema/entities.dart # moor/test/data/tables/custom_tables.g.dart # moor/test/data/tables/tables.moor # moor/test/parsed_sql/moor_files_integration_test.dart # moor_generator/lib/src/analyzer/moor/parser.dart # moor_generator/lib/src/analyzer/runner/steps.dart # moor_generator/lib/src/analyzer/runner/steps/analyze_dart.dart # moor_generator/lib/src/model/specified_db_classes.dart # moor_generator/lib/src/writer/database_writer.dart # sqlparser/lib/src/ast/ast.dart # sqlparser/lib/src/ast/statements/create_table.dart # sqlparser/lib/src/ast/statements/statement.dart # sqlparser/lib/src/reader/parser/schema.dart # sqlparser/lib/src/reader/tokenizer/token.dart # sqlparser/test/engine/autocomplete/static_test.dart
This commit is contained in:
commit
ba603f22cc
|
@ -1,17 +1,18 @@
|
|||
# Run tasks with the dart SDK installed by default
|
||||
container:
|
||||
image: "google/dart:latest"
|
||||
|
||||
# We're currently not running tests with coverage because the free cirrus containers run out of memory :(
|
||||
dockerfile: tool/Dockerfile
|
||||
|
||||
task:
|
||||
pub_cache:
|
||||
folder: $HOME/.pub-cache
|
||||
environment:
|
||||
CODECOV_TOKEN: ENCRYPTED[6322a159d9b7692b999d13fa2bc7981c8b61ecb1ac18ae076864f1355ee9b65088b2bf9d97d7860196e58bf1db5708af]
|
||||
dart_version_script: dart --version
|
||||
pub_get_script: tool/pub_get.sh
|
||||
analyze_script: tool/analyze.sh
|
||||
test_moor_script: tool/test_moor.sh
|
||||
test_sqlparser_script: tool/test_sqlparser.sh
|
||||
test_moor_ffi_script: tool/test_moor_ffi.sh
|
||||
vm_integration_test_script: tool/vm_integration_test.sh
|
||||
test_moor_generator_script: tool/test_generator.sh
|
||||
format_coverage_script: tool/upload_coverage.sh
|
|
@ -1,4 +1,10 @@
|
|||
**/.idea
|
||||
**/*.iml
|
||||
|
||||
lcov.info
|
||||
lcov.info
|
||||
|
||||
.packages
|
||||
pubspec.lock
|
||||
.dart_tool/
|
||||
|
||||
benchmark_results.json
|
|
@ -15,6 +15,10 @@ most certainly helps to resolve the issue quickly.
|
|||
All kinds of pull requests are absolutely appreciated! Before working on bigger changes, it
|
||||
can be helpful to create an issue describing your plans to help coordination.
|
||||
|
||||
When working on moor, its recommended to fork the `develop` branch and also target that
|
||||
branch for PRs. When possible, we only use the `master` branch to reflect the state that's
|
||||
been released to pub.
|
||||
|
||||
If you have any question about moor internals that you feel are not explained well enough,
|
||||
you're most welcome to create an issue or [chat via gitter](http://gitter.im/simolus3).
|
||||
|
||||
|
@ -66,13 +70,19 @@ used to construct common queries.
|
|||
## Workflows
|
||||
|
||||
### Debugging the analyzer plugin
|
||||
The analyzer plugin (branded as "SQL IDE" to users) can be run in isolation, which makes
|
||||
debugging easier. Note: Port 9999 has to be free for this to work, but you can change the
|
||||
|
||||
We have an analyzer plugin to support IDE features like auto-complete, navigation, syntax
|
||||
highlighting, outline and folding to users. Normally, analyzer plugins are discovered and
|
||||
loaded by the analysis server, which makes them very annoying to debug.
|
||||
|
||||
However, we found a way to run the plugin in isolation, which makes debugging much easier.
|
||||
Note: Port 9999 has to be free for this to work, but you can change the
|
||||
port defined in the two files below.
|
||||
|
||||
To debug the plugin, do the following:
|
||||
1. In `moor/tools/analyzer_plugin/bin/plugin.dart`, set `useDebuggingVariant` to true.
|
||||
2. Run `moor_generator/lib/plugin.dart` as a regular Dart VM app (this can be debugged).
|
||||
2. Run `moor_generator/bin/moor_generator.dart debug-plugin` as a regular Dart VM app
|
||||
(this can be debugged when started from an IDE).
|
||||
3. (optional) Make sure the analysis server picks up the updated version of the analysis
|
||||
plugin by deleting the `~/.dartServer/.plugin_manager` folder.
|
||||
4. Open a project that uses the plugin, for instance via `code extras/plugin_example`.
|
||||
|
|
43
README.md
43
README.md
|
@ -1,21 +1,48 @@
|
|||
# Moor
|
||||
[](https://cirrus-ci.com/github/simolus3/moor)
|
||||
[](https://codecov.io/gh/simolus3/moor)
|
||||
|
||||
[](https://gitter.im/moor-dart/community)
|
||||
|
||||
| Core | Flutter | Generator |
|
||||
|:-------------:|:-------------:|:-----:|
|
||||
| [](https://pub.dev/packages/moor) | [](https://pub.dev/packages/moor_flutter) | [](https://pub.dev/packages/moor_generator) |
|
||||
|
||||
Moor is an easy to use, reactive persistence library for Flutter and Dart web apps.
|
||||
Define your database tables in pure Dart and enjoy a fluent query API, auto-updating
|
||||
streams and more!
|
||||
Moor is a reactive persistence library for Flutter and Dart, built ontop of
|
||||
sqlite.
|
||||
Moor is
|
||||
|
||||
For more information, check out the [docs](https://moor.simonbinder.eu/).
|
||||
- __Flexible__: Moor let's you write queries in both SQL and Dart,
|
||||
providing fluent apis for both languages. You can filter and order results
|
||||
or use joins to run queries on multiple tables. You can even use complex
|
||||
sql features like `WITH` and `WINDOW` clauses.
|
||||
- __🔥 Feature rich__: Moor has builtin support for transactions, schema
|
||||
migrations, complex filters and expressions, batched updates and joins. We
|
||||
even have a builtin IDE for SQL!
|
||||
- __📦 Modular__: Thanks to builtin support for daos and `import`s in sql files, moor helps you keep your database code simple.
|
||||
- __🛡️ Safe__: Moor generates typesafe code based on your tables and queries. If you make a mistake in your queries, moor will find it at compile time and
|
||||
provide helpful and descriptive lints.
|
||||
- __⚡ Fast__: Even though moor lets you write powerful queries, it can keep
|
||||
up with the performance of key-value stores like shared preferences and Hive. Moor is the only major persistence library with builtin threading support, allowing you to run database code across isolates with zero additional effort.
|
||||
- __Reactive__: Turn any sql query into an auto-updating stream! This includes complex queries across many tables
|
||||
- __⚙️ Cross-Platform support__: Moor works on Android, iOS, macOS, Windows, Linux and the web. [This template](https://github.com/appleeducate/moor_shared) is a Flutter todo app that works on all platforms
|
||||
- __🗡️ Battle tested and production ready__: Moor is stable and well tested with a wide range of unit and integration tests. It powers production Flutter apps.
|
||||
|
||||
With moor, persistence on Flutter is fun!
|
||||
|
||||
__To start using moor, read our detailed [docs](https://moor.simonbinder.eu/docs/getting-started/).__
|
||||
|
||||
If you have any questions, feedback or ideas, feel free to [create an
|
||||
issue](https://github.com/simolus3/moor/issues/new). If you enjoy this
|
||||
project, I'd appreciate your [🌟 on GitHub](https://github.com/simolus3/moor/).
|
||||
|
||||
-----
|
||||
|
||||
The `sqlparser` directory contains an sql parser and static analyzer, written in pure Dart.
|
||||
At the moment, it can only parse a subset of sqlite, but most commonly used statements
|
||||
(except for inserts) are supported. Its on pub at
|
||||
Packages in this repo:
|
||||
- `moor`: The main rutime for moor, which provides most apis
|
||||
- `moor_ffi`: New and faster executor for moor, built with `dart:ffi`.
|
||||
- `moor_flutter`: The standard executor wrapping the `sqflite` package
|
||||
- `moor_generator`: The compiler for moor tables, databases and daos. It
|
||||
also contains a fully-featured sql ide
|
||||
- `sqlparser`: A sql parser and static analyzer, written in pure Dart. This package can be used without moor to perform analysis on sql statements.
|
||||
It's on pub at
|
||||
[](https://pub.dev/packages/sqlparser)
|
|
@ -6,81 +6,168 @@ analyzer:
|
|||
unused_import: error
|
||||
unused_local_variable: error
|
||||
dead_code: error
|
||||
override_on_non_overriding_method: error
|
||||
public_member_api_docs: ignore # turned on by user-facing subpackages
|
||||
exclude:
|
||||
- "**/*.g.dart"
|
||||
# Will be analyzed anyway, nobody knows why ¯\_(ツ)_/¯. We're only analyzing lib/ and test/ as a workaround
|
||||
- ".dart_tool/build/entrypoint/build.dart"
|
||||
- "tool/**"
|
||||
|
||||
# this should always include all rules. Those we don't use are commented out
|
||||
linter:
|
||||
rules:
|
||||
- annotate_overrides
|
||||
# ERROR RULES
|
||||
- avoid_empty_else
|
||||
- avoid_function_literals_in_foreach_calls
|
||||
- avoid_init_to_null
|
||||
- avoid_null_checks_in_equality_operators
|
||||
# - avoid_print (all our prints can be turned off)
|
||||
- avoid_relative_lib_imports
|
||||
- avoid_returning_null_for_future
|
||||
# - avoid_slow_async_io
|
||||
- avoid_types_as_parameter_names
|
||||
- cancel_subscriptions
|
||||
- close_sinks
|
||||
- comment_references
|
||||
- control_flow_in_finally
|
||||
# - diagnostic_describe_all_properties (Flutter-specific, not relevant for us)
|
||||
- empty_statements
|
||||
- hash_and_equals
|
||||
# - invariant_booleans (turned off because the lint rule is buggy)
|
||||
- iterable_contains_unrelated_type
|
||||
- list_remove_unrelated_type
|
||||
- literal_only_boolean_expressions
|
||||
- no_adjacent_strings_in_list
|
||||
- no_duplicate_case_values
|
||||
# - prefer_relative_imports (clashes with avoid_relative_lib_imports)
|
||||
# - prefer_void_to_null (we do use Null as a type for alwaysThrows functions)
|
||||
- test_types_in_equals
|
||||
- throw_in_finally
|
||||
- unnecessary_statements
|
||||
- unrelated_type_equality_checks
|
||||
- unsafe_html
|
||||
- valid_regexps
|
||||
# STYLE RULES
|
||||
- always_declare_return_types
|
||||
# - always_put_control_body_on_new_line (we don't do this if it fits on the same line)
|
||||
# - always_put_required_named_parameters_first (we just don't do this)
|
||||
# - always_require_non_null_named_parameters (we don't use assert foo != null for parameters)
|
||||
# - always_specify_types (we prefer to omit the type parameter when possible)
|
||||
- annotate_overrides
|
||||
# - avoid_annotating_with_dynamic (we prefer to make dynamic explicit)
|
||||
# - avoid_as (we prefer to make explicit casts explicit!)
|
||||
- avoid_bool_literals_in_conditional_expressions
|
||||
# - avoid_catches_without_on_clauses (we have a lot of generic catches)
|
||||
- avoid_catching_errors
|
||||
- avoid_classes_with_only_static_members
|
||||
- avoid_double_and_int_checks
|
||||
# - avoid_equals_and_hash_code_on_mutable_classes (lint is to generic for transient fields)
|
||||
- avoid_field_initializers_in_const_classes
|
||||
- avoid_function_literals_in_foreach_calls
|
||||
# - avoid_implementing_value_types (maybe we can consider turning this on?)
|
||||
- avoid_init_to_null
|
||||
- avoid_js_rounded_ints
|
||||
- avoid_null_checks_in_equality_operators
|
||||
# - avoid_positional_boolean_parameters (there pretty useful when there's only one boolean param)
|
||||
# - avoid_private_typedef_functions (they're still useful)
|
||||
- avoid_renaming_method_parameters
|
||||
- avoid_return_types_on_setters
|
||||
- avoid_returning_null
|
||||
- avoid_types_as_parameter_names
|
||||
- avoid_returning_null_for_void
|
||||
- avoid_returning_this
|
||||
- avoid_setters_without_getters
|
||||
- avoid_shadowing_type_parameters
|
||||
- avoid_single_cascade_in_expression_statements
|
||||
# - avoid_types_on_closure_parameters (the interference isn't THAT good)
|
||||
# - avoid_unnecessary_containers (Flutter-specific, not relevant here)
|
||||
- avoid_unused_constructor_parameters
|
||||
- avoid_void_async
|
||||
- await_only_futures
|
||||
- camel_case_extensions
|
||||
- camel_case_types
|
||||
- cancel_subscriptions
|
||||
- comment_references
|
||||
# - cascade_invocations (sometimes the explicit notation is more readable)
|
||||
- constant_identifier_names
|
||||
- curly_braces_in_flow_control_structures
|
||||
- control_flow_in_finally
|
||||
- directives_ordering
|
||||
- empty_catches
|
||||
- empty_constructor_bodies
|
||||
- empty_statements
|
||||
- hash_and_equals
|
||||
- file_names
|
||||
# - flutter_style_todos (Flutter-development specific, not relevant here)
|
||||
- implementation_imports
|
||||
- invariant_booleans
|
||||
- iterable_contains_unrelated_type
|
||||
- join_return_with_assignment
|
||||
- library_names
|
||||
- library_prefixes
|
||||
- list_remove_unrelated_type
|
||||
- no_adjacent_strings_in_list
|
||||
- no_duplicate_case_values
|
||||
- lines_longer_than_80_chars
|
||||
- non_constant_identifier_names
|
||||
- null_closures
|
||||
- omit_local_variable_types
|
||||
# - one_member_abstracts (there are cases where a one-member abstract class makes sense, see moor's Insertable)
|
||||
- only_throw_errors
|
||||
- overridden_fields
|
||||
- package_api_docs
|
||||
- package_names
|
||||
- package_prefixed_library_names
|
||||
# - package_prefixed_library_names (this isn't java)
|
||||
# - parameter_assignments (we regularly use this to set default values)
|
||||
- prefer_adjacent_string_concatenation
|
||||
- prefer_asserts_in_initializer_lists
|
||||
# - prefer_asserts_with_message (it's annoying to write messages for internal invariants)
|
||||
- prefer_collection_literals
|
||||
- prefer_conditional_assignment
|
||||
- prefer_const_constructors
|
||||
- prefer_const_constructors_in_immutables
|
||||
- prefer_const_declarations
|
||||
- prefer_const_literals_to_create_immutables
|
||||
- prefer_constructors_over_static_methods
|
||||
- prefer_contains
|
||||
# - prefer_double_quotes (we prefer single quotes)
|
||||
- prefer_equal_for_default_values
|
||||
# - prefer_expression_function_bodies (for multiline expressions, this is ugly to format)
|
||||
- prefer_final_fields
|
||||
- prefer_final_in_for_each
|
||||
- prefer_final_locals
|
||||
- prefer_for_elements_to_map_fromIterable
|
||||
- prefer_foreach
|
||||
- prefer_function_declarations_over_variables
|
||||
- prefer_generic_function_type_aliases
|
||||
- prefer_if_elements_to_conditional_expressions
|
||||
- prefer_if_null_operators
|
||||
- prefer_initializing_formals
|
||||
- prefer_inlined_adds
|
||||
- prefer_int_literals
|
||||
- prefer_interpolation_to_compose_strings
|
||||
- prefer_is_empty
|
||||
- prefer_is_not_empty
|
||||
- prefer_is_not_operator
|
||||
- prefer_iterable_whereType
|
||||
# - prefer_mixin (todo we could consider enabling this)
|
||||
- prefer_null_aware_operators
|
||||
- prefer_single_quotes
|
||||
- prefer_spread_collections
|
||||
- prefer_typing_uninitialized_variables
|
||||
- provide_deprecation_message
|
||||
- public_member_api_docs
|
||||
- recursive_getters
|
||||
- slash_for_doc_comments
|
||||
- test_types_in_equals
|
||||
- throw_in_finally
|
||||
# - sort_child_properties_last (Flutter specific)
|
||||
# - sort_constructors_first (we don't do this)
|
||||
# - sort_unnamed_constructors_first
|
||||
- type_annotate_public_apis
|
||||
- type_init_formals
|
||||
- unawaited_futures
|
||||
- unnecessary_brace_in_string_interps
|
||||
- unnecessary_const
|
||||
# - unnecessary_final (we prefer final here)
|
||||
- unnecessary_getters_setters
|
||||
- unnecessary_lambdas
|
||||
- unnecessary_new
|
||||
- unnecessary_null_aware_assignments
|
||||
- unnecessary_statements
|
||||
- unnecessary_null_in_if_null_operators
|
||||
- unnecessary_overrides
|
||||
- unnecessary_parenthesis
|
||||
- unnecessary_this
|
||||
- unrelated_type_equality_checks
|
||||
# - use_full_hex_values_for_flutter_colors (Flutter specific)
|
||||
- use_function_type_syntax_for_parameters
|
||||
- use_rethrow_when_possible
|
||||
- valid_regexps
|
||||
- public_member_api_docs
|
||||
- use_setters_to_change_properties
|
||||
- use_string_buffers
|
||||
# - use_to_and_as_if_applicable (false positive on operators)
|
||||
- void_checks
|
||||
# PUB RULES
|
||||
- package_names
|
||||
# - sort_pub_dependencies (we prefer to group them by what they do)
|
|
@ -37,7 +37,7 @@ blog = "/:section/:year/:month/:day/:slug/"
|
|||
## Configuration for BlackFriday markdown parser: https://github.com/russross/blackfriday
|
||||
[blackfriday]
|
||||
plainIDAnchors = true
|
||||
hrefTargetBlank = true
|
||||
hrefTargetBlank = false
|
||||
angledQuotes = false
|
||||
latexDashes = true
|
||||
|
||||
|
@ -116,8 +116,8 @@ navbar_logo = false
|
|||
url = "mailto:oss@simonbinder.eu"
|
||||
icon = "fa fa-envelope"
|
||||
[[params.links.user]]
|
||||
name = "Contact me via gitter"
|
||||
url = "https://gitter.im/simolus3"
|
||||
name = "Room in gitter"
|
||||
url = "https://gitter.im/moor-dart/community"
|
||||
icon = "fab fa-gitter"
|
||||
[[params.links.user]]
|
||||
name = "Project on GitHub"
|
||||
|
|
|
@ -13,14 +13,19 @@ linkTitle: "Moor"
|
|||
<a class="btn btn-lg btn-secondary mr-3 mb-4" href="https://pub.dev/packages/moor_flutter">
|
||||
Get fom pub <i class="fas fa-code ml-2 "></i>
|
||||
</a>
|
||||
|
||||
<p class="lead mt-5">
|
||||
With a fluent query api, a powerful sql analyzer, auto-updating streams and much moor,
|
||||
moor makes persistence fun. Scroll down to learn about moor's key features, or visit the
|
||||
<a href="{{< relref "/docs/Getting started" >}}">getting started guide</a> for a step-by-step guide on using moor.
|
||||
</p>
|
||||
</div>
|
||||
{{< /blocks/cover >}}
|
||||
|
||||
|
||||
{{% blocks/lead color="dark" %}}
|
||||
Moor is an easy to use, reactive persistence library for Flutter apps. Define your
|
||||
database tables in pure Dart and enjoy a fluent query API, auto-updating streams
|
||||
and more!
|
||||
Moor is an easy to use, reactive persistence library for Flutter apps. Define tables in Dart or
|
||||
SQL and enjoy a fluent query API, auto-updating streams and more!
|
||||
{{% /blocks/lead %}}
|
||||
|
||||
{{< blocks/section color="primary" >}}
|
||||
|
@ -42,9 +47,32 @@ code.
|
|||
|
||||
|
||||
{{% blocks/feature icon="fas fa-star" title="And much more!" %}}
|
||||
Moor provides auto-updating `Streams` for all your queries, makes dealing with transactions and migrations easy
|
||||
Moor provides auto-updating streams for all your queries, makes dealing with transactions and migrations easy
|
||||
and lets your write modular database code with DAOs. We even have a [sql IDE](too) builtin to the project
|
||||
When using moor, working with databases in Dart is fun!
|
||||
|
||||
{{% /blocks/feature %}}
|
||||
{{< /blocks/section >}}
|
||||
|
||||
{{< blocks/section color="light" type="section" >}}
|
||||
|
||||
<h2>Key moor features</h2>
|
||||
Here are some of the many ways moor helps you write awesome database code:
|
||||
<ul>
|
||||
<li><b>Auto-updating streams</b>: With moor, any query - no matter how complex - can be turned into a stream that emits new data as the underlying data changes.</li>
|
||||
<li><b>Polyglot</b>: Moor lets you write queries in a fluent Dart api or directly in SQL - you can even embed Dart expressions in SQL.</li>
|
||||
<li><b>Safety</b>: Moor can verify your table declarations and queries at compile time, providing helpful and descriptive hints when it finds problems.</li>
|
||||
<li><b>Boilerplate-free</b>: Stop writing mapping code yourself - moor can take of that. Moor generates Dart code around your data so you can focus
|
||||
on building great apps.</li>
|
||||
<li><b>Flexible</b>: Want to write queries in SQL? Moor verifies them at compile time and generates Dart apis for them. Prefer to write them in Dart?
|
||||
Moor will generate efficient SQL for Dart queries.</li>
|
||||
<li><b>Easy to learn</b>: Instead of having to learn yet another ORM, moor lets you write queries in SQL and generates typesafe wrappers. Queries and tables
|
||||
can also be written in Dart that looks similar to SQL without loosing type-safety.</li>
|
||||
<li><b>Fast _and_ powerful</b>: With the new `moor_ffi` backend, moor can outperform key-value stores without putting any compromises on the integrity
|
||||
and flexibility that relational databases provide. Moor is the only major persistence library with builtin support for multiple isolates.</li>
|
||||
<li><b>Well tested and production ready</b>: Each component of moor is verified by a wide range of unit and integration tests. Moor powers many Flutter apps
|
||||
in production.</li>
|
||||
<li><b>Cross-Platform</b>: Moor works on iOS, Android, Linux, macOS, Windows and on the web. It doesn't even require Flutter.</li>
|
||||
</ul>
|
||||
|
||||
{{< /blocks/section >}}
|
|
@ -18,9 +18,11 @@ targets:
|
|||
builders:
|
||||
moor_generator:
|
||||
options:
|
||||
write_from_json_string_constructor: true
|
||||
compact_query_methods: true
|
||||
```
|
||||
|
||||
## Available options
|
||||
|
||||
At the moment, moor supports these options:
|
||||
|
||||
* `write_from_json_string_constructor`: boolean. Adds a `.fromJsonString` factory
|
||||
|
@ -48,6 +50,32 @@ At the moment, moor supports these options:
|
|||
(so a column named `user_name` would also use `user_name` as a json key instead of `userName`).
|
||||
This will be the only option in moor 3.0. You can always override the json key by using a `JSON KEY`
|
||||
column constraint (e.g. `user_name VARCHAR NOT NULL JSON KEY userName`)
|
||||
* `generate_connect_constructor`: Generate necessary code to support the [isolate runtime]({{< relref "isolates.md" >}}).
|
||||
This is a build option because isolates are still experimental. This will be the default option eventually.
|
||||
* `sqlite_modules`: This list can be used to enable sqlite extensions, like those for json or full-text search.
|
||||
Modules have to be enabled explicitly because they're not supported on all platforms. See the following section for
|
||||
details.
|
||||
|
||||
## Available extensions
|
||||
|
||||
__Note__: This enables extensions in the analyzer for custom queries only. For instance, when the `json1` extension is
|
||||
enabled, the [`json`](https://www.sqlite.org/json1.html) functions can be used in moor files. This doesn't necessarily
|
||||
mean that those functions are supported at runtime! Both extensions are available on iOS 11 or later. On Android, they're
|
||||
only available when using `moor_ffi`. See [our docs]({{< relref "extensions.md" >}}) for more details on them.
|
||||
|
||||
```yaml
|
||||
targets:
|
||||
$default:
|
||||
builders:
|
||||
moor_generator:
|
||||
options:
|
||||
sqlite_module:
|
||||
- json1
|
||||
- fts5
|
||||
```
|
||||
|
||||
We currently support the [json1](https://www.sqlite.org/json1.html) and [fts5](https://www.sqlite.org/fts5.html) extensions
|
||||
for static analysis. Feel free to create an issue if you need support for different extensions.
|
||||
|
||||
## Recommended options
|
||||
|
||||
|
|
|
@ -0,0 +1,174 @@
|
|||
---
|
||||
title: "Isolates"
|
||||
description: Seamlessly run moor on a background isolate and unblock the main thread
|
||||
---
|
||||
|
||||
{{% alert title="New feature" color="primary" %}}
|
||||
The api for background isolates only works with moor version 2.1.0 or newer. Due to
|
||||
platform limitations, using [moor_ffi]({{< relref "../Other engines/vm.md" >}}) is required when
|
||||
using a background isolate. Using `moor_flutter` is not supported.
|
||||
{{% /alert %}}
|
||||
|
||||
## Preparations
|
||||
|
||||
To use the isolate api, first enable the appropriate [build option]({{< relref "builder_options.md" >}}) by
|
||||
creating a file called `build.yaml` in your project root, next to your `pubspec.yaml`. It should have the following
|
||||
content:
|
||||
```yaml
|
||||
targets:
|
||||
$default:
|
||||
builders:
|
||||
moor_generator:
|
||||
options:
|
||||
generate_connect_constructor: true
|
||||
```
|
||||
Next, re-run the build. You can now add another constructor to the generated database class:
|
||||
```dart
|
||||
@UseMoor(...)
|
||||
class TodoDb extends _$TodoDb {
|
||||
TodoDb() : super(VmDatabase.memory());
|
||||
|
||||
// this is the new constructor
|
||||
TodoDb.connect(DatabaseConnection connection) : super.connect(connection);
|
||||
}
|
||||
```
|
||||
|
||||
## Using moor in a background isolate
|
||||
|
||||
With the database class ready, let's open it on a background isolate
|
||||
```dart
|
||||
import 'package:moor/isolates.dart';
|
||||
|
||||
// This needs to be a top-level method because it's run on a background isolate
|
||||
DatabaseConnection _backgroundConnection() {
|
||||
// construct the database. You can also wrap the VmDatabase in a "LazyDatabase" if you need to run
|
||||
// work before the database opens.
|
||||
final database = VmDatabase.memory();
|
||||
return DatabaseConnection.fromExecutor(database);
|
||||
}
|
||||
|
||||
void main() async {
|
||||
// create a moor executor in a new background isolate. If you want to start the isolate yourself, you
|
||||
// can also call MoorIsolate.inCurrent() from the background isolate
|
||||
MoorIsolate isolate = await MoorIsolate.spawn(_backgroundConnection);
|
||||
|
||||
// we can now create a database connection that will use the isolate internally. This is NOT what's
|
||||
// returned from _backgroundConnection, moor uses an internal proxy class for isolate communication.
|
||||
DatabaseConnection connection = await isolate.connect();
|
||||
|
||||
final db = TodoDb.connect(connection);
|
||||
|
||||
// you can now use your database exactly like you regularly would, it transparently uses a
|
||||
// background isolate internally
|
||||
}
|
||||
```
|
||||
|
||||
### Initialization on the main thread
|
||||
|
||||
Platform channels are not available on background isolates, but sometimes you might want to use
|
||||
a function like `getApplicationDocumentsDirectory` from `path_provider` to construct the database
|
||||
path. As this function uses a method channel internally, we have to use a trick to initialize the
|
||||
database.
|
||||
We're going to start the isolate running the database manually. This allows us to pass additional
|
||||
data that we calculated on the main thread.
|
||||
|
||||
```dart
|
||||
Future<MoorIsolate> _createMoorIsolate() async {
|
||||
// this method is called from the main isolate. Since we can't use
|
||||
// getApplicationDocumentsDirectory on a background isolate, we calculate
|
||||
// the database path in the foreground isolate and then inform the
|
||||
// background isolate about the path.
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
final path = p.join(dir.path, 'db.sqlite');
|
||||
final receivePort = ReceivePort();
|
||||
|
||||
await Isolate.spawn(
|
||||
_startBackground,
|
||||
_IsolateStartRequest(receivePort.sendPort, path),
|
||||
);
|
||||
|
||||
// _startBackground will send the MoorIsolate to this ReceivePort
|
||||
return (await receivePort.first as MoorIsolate);
|
||||
}
|
||||
|
||||
void _startBackground(_IsolateStartRequest request) {
|
||||
// this is the entrypoint from the background isolate! Let's create
|
||||
// the database from the path we received
|
||||
final executor = VmDatabase(File(request.targetPath));
|
||||
// we're using MoorIsolate.inCurrent here as this method already runs on a
|
||||
// background isolate. If we used MoorIsolate.spawn, a third isolate would be
|
||||
// started which is not what we want!
|
||||
final moorIsolate = MoorIsolate.inCurrent(
|
||||
() => DatabaseConnection.fromExecutor(executor),
|
||||
);
|
||||
// inform the starting isolate about this, so that it can call .connect()
|
||||
request.sendMoorIsolate.send(moorIsolate);
|
||||
}
|
||||
|
||||
// used to bundle the SendPort and the target path, since isolate entrypoint
|
||||
// functions can only take one parameter.
|
||||
class _IsolateStartRequest {
|
||||
final SendPort sendMoorIsolate;
|
||||
final String targetPath;
|
||||
|
||||
_IsolateStartRequest(this.sendMoorIsolate, this.targetPath);
|
||||
}
|
||||
```
|
||||
|
||||
### Shutting down the isolate
|
||||
|
||||
Since multiple `DatabaseConnection`s can exist to a specific `MoorIsolate`, simply calling
|
||||
`Database.close` won't stop the isolate. You can use the `MoorIsolate.shutdownAll()` for that.
|
||||
It will disconnect all databases and then close the background isolate, releasing all resources.
|
||||
|
||||
## Common operation modes
|
||||
|
||||
The `MoorIsolate` object itself can be sent across isolates, so if you have more than one isolate
|
||||
from which you want to use moor, that's no problem!
|
||||
|
||||
__One executor isolate, one foreground isolate__: This is the most common usage mode. You would call
|
||||
`MoorIsolate.spawn` from the `main` method in your Flutter or Dart app. Similar to the example above,
|
||||
you could then use moor from the main isolate by connecting with `MoorIsolate.connect` and passing that
|
||||
connection to a generated database class.
|
||||
|
||||
__One executor isolate, multiple client isolates__: The `MoorIsolate` can be sent across multiple
|
||||
isolates, each of which can use `MoorIsolate.connect` on their own. This is useful to implement
|
||||
a setup where you have three or more threads:
|
||||
|
||||
- The moor executor isolate
|
||||
- A foreground isolate, probably for Flutter
|
||||
- Another background isolate, which could be used for networking.
|
||||
|
||||
You can the read data from the foreground isolate or start query streams, similar to the example
|
||||
above. The background isolate would _also_ call `MoorIsolate.connect` and create its own instance
|
||||
of the generated database class. Writes to one database will be visible to the other isolate and
|
||||
also update query streams.
|
||||
|
||||
## How does this work? Are there any limitations?
|
||||
|
||||
All moor features are supported on background isolates and work out of the box. This includes
|
||||
|
||||
- Transactions
|
||||
- Auto-updating queries (even if the table was updated from another isolate)
|
||||
- Batched updates and inserts
|
||||
- Custom statements or those generated from an sql api
|
||||
|
||||
Please note that, will using a background isolate can reduce lag on the UI thread, the overall
|
||||
database is going to be slower! There's a overhead involved in sending data between
|
||||
isolates, and that's exactly what moor has to do internally. If you're not running into dropped
|
||||
frames because of moor, using a background isolate is probably not necessary for your app.
|
||||
|
||||
Internally, moor uses the following model to implement this api:
|
||||
|
||||
- __A server isolate__: A single isolate that executes all queries and broadcasts tables updates.
|
||||
This is the isolate created by `MoorIsolate.spawn`. It supports any number of clients via an
|
||||
rpc-like connection model. Connections are established via `SendPort`s and `ReceivePort`s.
|
||||
Internally, the `MoorIsolate` class only contains a reference to a `SendPort` that can be used to
|
||||
establish a connection to the background isolate. This lets users share the `MoorIsolate`
|
||||
object across many isolates and connect multiple times. The actual server logic that listens on
|
||||
the port is in a private `_MoorServer` class.
|
||||
- __Client isolates__: Any number of clients in any number of isolates can connect to a `MoorIsolate`.
|
||||
The client acts as a moor backend, which means that all queries are built on the client isolate. The
|
||||
raw sql string and parameters are then sent to the server isolate, which will enqueue the operation
|
||||
and execute it eventually. Implementing the isolate commands at a low level allows users to re-use
|
||||
all their code used without the isolate api.
|
|
@ -1,11 +1,14 @@
|
|||
---
|
||||
title: "Joins"
|
||||
title: "Advanced queries in Dart"
|
||||
weight: 1
|
||||
description: Use joins to write queries that read from more than one table
|
||||
description: Use sql joins or custom expressions from the Dart api
|
||||
url: /docs/advanced-features/joins
|
||||
aliases:
|
||||
- /queries/joins
|
||||
---
|
||||
|
||||
## Joins
|
||||
|
||||
Moor supports sql joins to write queries that operate on more than one table. To use that feature, start
|
||||
a select regular select statement with `select(table)` and then add a list of joins using `.join()`. For
|
||||
inner and left outer joins, a `ON` expression needs to be specified. Here's an example using the tables
|
||||
|
@ -49,7 +52,37 @@ return query.watch().map((rows) {
|
|||
```
|
||||
|
||||
_Note_: `readTable` returns `null` when an entity is not present in the row. For instance, todo entries
|
||||
might not be in any category. If we a row without a category, `row.readTable(categories)` would return `null`.
|
||||
might not be in any category.For a row without a category, `row.readTable(categories)` would return `null`.
|
||||
|
||||
## Custom columns
|
||||
|
||||
Select statements aren't limited to columns from tables. You can also include more complex expressions in the
|
||||
query. For each row in the result, those expressions will be evaluated by the database engine.
|
||||
|
||||
```dart
|
||||
class EntryWithImportance {
|
||||
final TodoEntry entry;
|
||||
final bool important;
|
||||
|
||||
EntryWithImportance(this.entry, this.important);
|
||||
}
|
||||
|
||||
Future<List<EntryWithImportance>> loadEntries() {
|
||||
// assume that an entry is important if it has the string "important" somewhere in its content
|
||||
final isImportant = todos.content.like('%important%');
|
||||
|
||||
return select(todos).addColumns([isImportant]).map((row) {
|
||||
final entry = row.readTable(todos);
|
||||
final entryIsImportant = row.read(isImportant);
|
||||
|
||||
return EntryWithImportance(entry, entryIsImportant);
|
||||
}).get();
|
||||
}
|
||||
```
|
||||
|
||||
Note that the `like` check is _not_ performed in Dart - it's sent to the underlying database engine which
|
||||
can efficiently compute it for all rows.
|
||||
|
||||
## Aliases
|
||||
Sometimes, a query references a table more than once. Consider the following example to store saved routes for a
|
||||
navigation system:
|
||||
|
|
|
@ -51,17 +51,17 @@ to run the statements.
|
|||
Starting from moor 1.5, you can use the `beforeOpen` parameter in the `MigrationStrategy` which will be called after
|
||||
migrations, but after any other queries are run. You could use it to populate data after the database has been created:
|
||||
```dart
|
||||
beforeOpen: (db, details) async {
|
||||
beforeOpen: (details) async {
|
||||
if (details.wasCreated) {
|
||||
final workId = await db.into(categories).insert(Category(description: 'Work'));
|
||||
final workId = await into(categories).insert(Category(description: 'Work'));
|
||||
|
||||
await db.into(todos).insert(TodoEntry(
|
||||
await into(todos).insert(TodoEntry(
|
||||
content: 'A first todo entry',
|
||||
category: null,
|
||||
targetDate: DateTime.now(),
|
||||
));
|
||||
|
||||
await db.into(todos).insert(
|
||||
await into(todos).insert(
|
||||
TodoEntry(
|
||||
content: 'Rework persistence code',
|
||||
category: workId,
|
||||
|
@ -72,12 +72,20 @@ beforeOpen: (db, details) async {
|
|||
```
|
||||
You could also activate pragma statements that you need:
|
||||
```dart
|
||||
beforeOpen: (db, details) async {
|
||||
beforeOpen: (details) async {
|
||||
if (details.wasCreated) {
|
||||
// ...
|
||||
}
|
||||
await db.customStatement('PRAGMA foreign_keys = ON');
|
||||
await customStatement('PRAGMA foreign_keys = ON');
|
||||
}
|
||||
```
|
||||
It is important that you run these queries on `db` explicitly. Failing to do so causes a deadlock which prevents the
|
||||
database from being opened.
|
||||
|
||||
## During development
|
||||
|
||||
During development, you might be changing your schema very often and don't want to write migrations for that
|
||||
yet. You can just delete your apps' data and reinstall the app - the database will be deleted and all tables
|
||||
will be created again. Please note that uninstalling is not enough sometimes - Android might have backed up
|
||||
the database file and will re-create it when installing the app again.
|
||||
|
||||
You can also delete and re-create all tables everytime your app is opened, see [this comment](https://github.com/simolus3/moor/issues/188#issuecomment-542682912)
|
||||
on how that can be achieved.
|
|
@ -11,31 +11,42 @@ aliases:
|
|||
_Note:_ If you prefer a tutorial video, Reso Coder has made a detailed video explaining
|
||||
how to get started. You can watch it [here](https://youtu.be/zpWsedYMczM).
|
||||
|
||||
|
||||
## Adding the dependency
|
||||
First, lets add moor to your project's `pubspec.yaml`.
|
||||
At the moment, the current version of `moor_flutter` is [](https://pub.dartlang.org/packages/moor_flutter) and the current version of `moor_generator` is [](https://pub.dartlang.org/packages/moor_generator)
|
||||
At the moment, the current version of `moor` is [](https://pub.dartlang.org/packages/moor),
|
||||
`moor_ffi` is at [](https://pub.dartlang.org/packages/moor_ffi)
|
||||
and the latest version of `moor_generator` is [](https://pub.dartlang.org/packages/moor_generator)
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
moor_flutter: # use the latest version
|
||||
moor: # use the latest version
|
||||
moor_ffi: # use the latest version
|
||||
path_provider:
|
||||
path:
|
||||
|
||||
dev_dependencies:
|
||||
moor_generator: # use the latest version
|
||||
build_runner:
|
||||
```
|
||||
We're going to use the `moor_flutter` library to specify tables and access the database. The
|
||||
`moor_generator` library will take care of generating the necessary code so the
|
||||
library knows what your table structure looks like.
|
||||
|
||||
If you're wondering why so many packages are necessary, here's a quick overview over what each package does:
|
||||
|
||||
- `moor`: This is the core package defining most apis
|
||||
- `moor_ffi`: Contains code that will run the actual queries
|
||||
- `path_provider` and `path`: Used to find a suitable location to store the database. Maintained by the Flutter and Dart team
|
||||
- `moor_generator`: Generates query code based on your tables
|
||||
- `build_runner`: Common tool for code-generation, maintained by the Dart team
|
||||
|
||||
{{% changed_to_ffi %}}
|
||||
|
||||
### Declaring tables
|
||||
Using moor, you can model the structure of your tables with simple dart code:
|
||||
```dart
|
||||
import 'package:moor_flutter/moor_flutter.dart';
|
||||
import 'package:moor/moor.dart';
|
||||
|
||||
// assuming that your file is called filename.dart. This will give an error at first,
|
||||
// but it's needed for moor to know about the generated code
|
||||
part 'filename.g.dart';
|
||||
part 'filename.g.dart';
|
||||
|
||||
// this will generate a table called "todos" for us. The rows of that table will
|
||||
// be represented by a class called "Todo".
|
||||
|
@ -77,14 +88,34 @@ After running either command once, the moor generator will have created a class
|
|||
database and data classes for your entities. To use it, change the `MyDatabase` class as
|
||||
follows:
|
||||
```dart
|
||||
// These imports are only needed to open the database
|
||||
import 'package:moor_ffi/moor_ffi.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
LazyDatabase _openConnection() {
|
||||
// the LazyDatabase util lets us find the right location for the file async.
|
||||
return LazyDatabase(() async {
|
||||
// put the database file, called db.sqlite here, into the documents folder
|
||||
// for your app.
|
||||
final dbFolder = await getApplicationDocumentsDirectory();
|
||||
final file = File(p.join(dbFolder, 'db.sqlite'));
|
||||
return VmDatabase(file);
|
||||
});
|
||||
}
|
||||
|
||||
@UseMoor(tables: [Todos, Categories])
|
||||
class MyDatabase extends _$MyDatabase {
|
||||
// we tell the database where to store the data with this constructor
|
||||
MyDatabase() : super(FlutterQueryExecutor.inDatabaseFolder(path: 'db.sqlite'));
|
||||
MyDatabase() : super(_openConnection());
|
||||
|
||||
// you should bump this number whenever you change or add a table definition. Migrations
|
||||
// are covered later in this readme.
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
int get schemaVersion => 1;
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
Congratulations! You're now ready to use all of moor. See the articles below for further reading.
|
||||
The ["Writing queries"]({{< relref "writing_queries.md" >}}) article contains everything you need
|
||||
to know to write selects, updates and inserts in moor!
|
|
@ -0,0 +1,96 @@
|
|||
---
|
||||
title: "Dart tables"
|
||||
description: Further information on Dart tables
|
||||
weight: 150
|
||||
---
|
||||
|
||||
{{% pageinfo %}}
|
||||
__Prefer sql?__: If you prefer, you can also declare tables via `CREATE TABLE` statements.
|
||||
Moor's sql analyzer will generate matching Dart code. [Details]({{< ref "starting_with_sql.md" >}}).
|
||||
{{% /pageinfo %}}
|
||||
|
||||
As shown in the [getting started guide]({{<relref "_index.md">}}), sql tables can be written in Dart:
|
||||
```dart
|
||||
class Todos extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get title => text().withLength(min: 6, max: 32)();
|
||||
TextColumn get content => text().named('body')();
|
||||
IntColumn get category => integer().nullable()();
|
||||
}
|
||||
```
|
||||
|
||||
In this article, we'll cover some advanced features of this syntax.
|
||||
|
||||
## Names
|
||||
|
||||
By default, moor uses the `snake_case` name of the Dart getter in the database. For instance, the
|
||||
table
|
||||
```dart
|
||||
class EnabledCategories extends Table {
|
||||
IntColumn get parentCategory => integer()();
|
||||
// ..
|
||||
}
|
||||
```
|
||||
|
||||
Would be generated as `CREATE TABLE enabled_categories (parent_category INTEGER NOT NULL)`.
|
||||
|
||||
To override the table name, simply override the `tableName` getter. An explicit name for
|
||||
columns can be provided with the `named` method:
|
||||
```dart
|
||||
class EnabledCategories extends Table {
|
||||
String get tableName => 'categories';
|
||||
|
||||
IntColumn get parentCategory => integer().named('parent')();
|
||||
}
|
||||
```
|
||||
|
||||
The updated class would be generated as `CREATE TABLE categories (parent INTEGER NOT NULL)`.
|
||||
|
||||
To update the name of a column when serializing data to json, annotate the getter with
|
||||
[`@JsonKey`](https://pub.dev/documentation/moor/latest/moor_web/JsonKey-class.html).
|
||||
|
||||
## Nullability
|
||||
|
||||
By default, columns may not contain null values. When you forgot to set a value in an insert,
|
||||
an exception will be thrown. When using sql, moor also warns about that at compile time.
|
||||
|
||||
If you do want to make a column nullable, just use `nullable()`:
|
||||
```dart
|
||||
class Items {
|
||||
IntColumn get category => integer().nullable();
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## Default values
|
||||
|
||||
You can set a default value for a column. When not explicitly set, the default value will
|
||||
be used when inserting a new row. To set a constant default value, use `withDefault`:
|
||||
|
||||
```dart
|
||||
class Preferences extends Table {
|
||||
TextColumn get name => integer().autoIncrement()();
|
||||
BoolColumn get enabled => boolean().withDefault(const Constant(false))();
|
||||
}
|
||||
```
|
||||
|
||||
When you later use `into(preferences).insert(PreferencesCompanion.forInsert(name: 'foo'));`, the new
|
||||
row will have its `enabled` column set to false (and not to null, as it normally would).
|
||||
|
||||
Of course, constants can only be used for static values. But what if you want to generate a dynamic
|
||||
default value for each column? For that, you can use `clientDefault`. It takes a function returning
|
||||
the desired default value. The function will be called for each insert. For instance, here's an
|
||||
example generating a random Uuid using the `uuid` package:
|
||||
```dart
|
||||
final _uuid = Uuid();
|
||||
|
||||
class Users extends Table {
|
||||
TextColumn get id => text().clientDefault(() => _uuid.v4())();
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Don't know when to use which? Prefer to use `withDefault` when the default value is constant, or something
|
||||
simple like `currentDate`. For more complicated values, like a randomly generated id, you need to use
|
||||
`clientDefault`. Internally, `withDefault` writes the default value into the `CREATE TABLE` statement. This
|
||||
can be more efficient, but doesn't suppport dynamic values.
|
|
@ -32,13 +32,28 @@ Future<List<Animal>> findAnimalsByLegs(int legCount) {
|
|||
```
|
||||
|
||||
## Boolean algebra
|
||||
You can nest boolean expressions by using the top-level `and`, `or` and `not` functions
|
||||
You can nest boolean expressions by using the `&`, `!` operators and the `not` method
|
||||
exposed by moor:
|
||||
```dart
|
||||
// find all animals that aren't mammals and have 4 legs
|
||||
select(animals)..where((a) => and(not(a.isMammal), a.amountOfLegs.equals(4)))
|
||||
select(animals)..where((a) => a.isMammal.not() & a.amountOfLegs.equals(4))
|
||||
```
|
||||
|
||||
## Arithmetic
|
||||
For `int` and `double` expressions, you can use the `+`, `-`, `*` and `/` operators. To
|
||||
run calculations between a sql expression and a Dart value, wrap it in a `Variable`:
|
||||
```dart
|
||||
Future<List<Product>> canBeBought(int amount, int price) {
|
||||
return (select(products)..where((p) {
|
||||
final totalPrice = p.price * Variable(amount);
|
||||
return totalPrice.isSmallerOrEqualValue(price);
|
||||
})).get();
|
||||
}
|
||||
```
|
||||
|
||||
String expressions define a `+` operator as well. Just like you would expect, it performs
|
||||
concatenation in sql.
|
||||
|
||||
## Nullability
|
||||
To check whether an expression returns null, you can use the top-level `isNull` function,
|
||||
which takes any expression and returns a boolean expression. The expression returned will
|
||||
|
@ -46,20 +61,21 @@ resolve to `true` if the inner expression resolves to null and `false` otherwise
|
|||
As you would expect, `isNotNull` works the other way around.
|
||||
|
||||
## Date and Time
|
||||
For columns and expressions that return a `DateTime`, you can use the top-level
|
||||
`year`, `month`, `day`, `hour`, `minute` and `second` functions to extract individual
|
||||
For columns and expressions that return a `DateTime`, you can use the
|
||||
`year`, `month`, `day`, `hour`, `minute` and `second` getters to extract individual
|
||||
fields from that date:
|
||||
```dart
|
||||
select(users)..where((u) => year(u.birthDate).isLessThan(1950))
|
||||
select(users)..where((u) => u.birthDate.year.isLessThan(1950))
|
||||
```
|
||||
|
||||
To obtain the current date or the current time as an expression, use the `currentDate`
|
||||
and `currentDateAndTime` constants provided by moor.
|
||||
|
||||
## `IN` and `NOT IN`
|
||||
You can check whether an expression is in a list of values by using the `isIn` function:
|
||||
You can check whether an expression is in a list of values by using the `isIn` and `isNotIn`
|
||||
methods:
|
||||
```dart
|
||||
select(animals)..where((a) => isIn(a.amountOfLegs, [3, 7, 4, 2]))
|
||||
select(animals)..where((a) => a.amountOfLegs.isIn([3, 7, 4, 2]);
|
||||
```
|
||||
|
||||
Again, the `isNotIn` function works the other way around.
|
||||
|
|
|
@ -9,29 +9,41 @@ declaring both tables and queries in Dart. This version will focus on how to use
|
|||
|
||||
## Adding the dependency
|
||||
First, lets add moor to your project's `pubspec.yaml`.
|
||||
At the moment, the current version of `moor_flutter` is [](https://pub.dartlang.org/packages/moor_flutter) and the current version of `moor_generator` is [](https://pub.dartlang.org/packages/moor_generator)
|
||||
At the moment, the current version of `moor` is [](https://pub.dartlang.org/packages/moor),
|
||||
`moor_ffi` is at [](https://pub.dartlang.org/packages/moor_ffi)
|
||||
and the latest version of `moor_generator` is [](https://pub.dartlang.org/packages/moor_generator)
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
moor_flutter: # use the latest version
|
||||
moor: # use the latest version
|
||||
moor_ffi: # use the latest version
|
||||
path_provider:
|
||||
path:
|
||||
|
||||
dev_dependencies:
|
||||
moor_generator: # use the latest version
|
||||
build_runner:
|
||||
```
|
||||
|
||||
The `moor_flutter` package will execute sql at runtime, while the
|
||||
`moor_generator` will generate typesafe Dart based on your SQL queries.
|
||||
If you're wondering why so many packages are necessary, here's a quick overview over what each package does:
|
||||
|
||||
- `moor`: This is the core package defining most apis
|
||||
- `moor_ffi`: Contains code that will run the actual queries
|
||||
- `path_provider` and `path`: Used to find a suitable location to store the database. Maintained by the Flutter and Dart team
|
||||
- `moor_generator`: Generates Dart apis for the sql queries and tables you wrote
|
||||
- `build_runner`: Common tool for code-generation, maintained by the Dart team
|
||||
|
||||
{{% changed_to_ffi %}}
|
||||
|
||||
## Declaring tables and queries
|
||||
|
||||
To declare tables and queries in sql, create a file called `tables.moor`
|
||||
next to your Dart files (for instance in `lib/database/tables.moor`).
|
||||
|
||||
You can put the `CREATE TABLE` statements for your queries in there.
|
||||
You can put `CREATE TABLE` statements for your queries in there.
|
||||
The following example creates two tables to model a todo-app. If you're
|
||||
migrating an existing project to moor, you would put the `CREATE TABLE`
|
||||
for your tables in there.
|
||||
migrating an existing project to moor, you can just copy the `CREATE TABLE`
|
||||
statements you've already written into this file.
|
||||
```sql
|
||||
-- this is the tables.moor file
|
||||
CREATE TABLE todos (
|
||||
|
@ -79,19 +91,36 @@ a file called `database.dart` next to the `tables.moor` file you wrote
|
|||
in the previous step.
|
||||
|
||||
```dart
|
||||
import 'package:moor_flutter/moor_flutter.dart';
|
||||
import 'package:moor/moor.dart';
|
||||
// These imports are only needed to open the database
|
||||
import 'package:moor_ffi/moor_ffi.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
part 'database.g.dart';
|
||||
|
||||
@UseMoor(
|
||||
// relative import for the moor file. Moor also supports `package:`
|
||||
// imports
|
||||
include: {'tables.moor'},
|
||||
)
|
||||
class AppDb extends _$AppDb {
|
||||
AppDb() : super(FlutterQueryExecutor.inDatabaseFolder('app.db'));
|
||||
AppDb() : super(_openConnection());
|
||||
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
}
|
||||
|
||||
LazyDatabase _openConnection() {
|
||||
// the LazyDatabase util lets us find the right location for the file async.
|
||||
return LazyDatabase(() async {
|
||||
// put the database file, called db.sqlite here, into the documents folder
|
||||
// for your app.
|
||||
final dbFolder = await getApplicationDocumentsDirectory();
|
||||
final file = File(p.join(dbFolder, 'db.sqlite'));
|
||||
return VmDatabase(file);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
To generate the `database.g.dart` file which contains the `_$AppDb`
|
||||
|
@ -126,7 +155,7 @@ a `.moor` file - moor will generate matching code for them as well.
|
|||
|
||||
## Learning more
|
||||
|
||||
Know that you know how to use moor together with sql, here are some
|
||||
Now that you know how to use moor together with sql, here are some
|
||||
further guides to help you learn more:
|
||||
|
||||
- The [SQL IDE]({{< relref "../Using SQL/sql_ide.md" >}}) that provides feedback on sql queries right in your editor.
|
||||
|
|
|
@ -42,7 +42,8 @@ stream using `watch()`.
|
|||
You can apply filters to a query by calling `where()`. The where method takes a function that
|
||||
should map the given table to an `Expression` of boolean. A common way to create such expression
|
||||
is by using `equals` on expressions. Integer columns can also be compared with `isBiggerThan`
|
||||
and `isSmallerThan`. You can compose expressions using `and(a, b), or(a, b)` and `not(a)`.
|
||||
and `isSmallerThan`. You can compose expressions using `a & b, a | b` and `a.not()`. For more
|
||||
details on expressions, see [this guide]({{< relref "expressions.md" >}}).
|
||||
### Limit
|
||||
You can limit the amount of results returned by calling `limit` on queries. The method accepts
|
||||
the amount of rows to return and an optional offset.
|
||||
|
@ -58,7 +59,7 @@ You can also reverse the order by setting the `mode` property of the `OrderingTe
|
|||
`OrderingMode.desc`.
|
||||
|
||||
### Single values
|
||||
If you now a query is never going to return more than one row, wrapping the result in a `List`
|
||||
If you know a query is never going to return more than one row, wrapping the result in a `List`
|
||||
can be tedious. Moor lets you work around that with `getSingle` and `watchSingle`:
|
||||
```dart
|
||||
Stream<TodoEntry> entryById(int id) {
|
||||
|
@ -70,6 +71,8 @@ If an entry with the provided id exists, it will be sent to the stream. Otherwis
|
|||
more than one entry (which is impossible in this case), an error will be added
|
||||
instead.
|
||||
|
||||
If you need more complex queries with joins or custom columns, see [this site]({{< relref "../Advanced Features/joins.md" >}}).
|
||||
|
||||
## Updates and deletes
|
||||
You can use the generated classes to update individual fields of any row:
|
||||
```dart
|
||||
|
@ -142,4 +145,4 @@ can be omitted. All other fields must be set and non-null. The `insert` method w
|
|||
otherwise.
|
||||
|
||||
Multiple inserts can be batched by using `insertAll` - it takes a list of companions instead
|
||||
of a single companion.
|
||||
of a single companion.
|
||||
|
|
|
@ -1,13 +1,9 @@
|
|||
---
|
||||
title: Dart VM (using ffi)
|
||||
description: Experimental version of moor using `dart:ffi`
|
||||
title: Dart VM (Desktop support)
|
||||
description: Run moor on both mobile and desktop
|
||||
---
|
||||
|
||||
## Warnings and supported platforms
|
||||
|
||||
Please note that `dart:ffi` is in "preview" at the moment and that there will be breaking
|
||||
changes. Using the `moor_ffi` package on non-stable Dart or Flutter versions can break.
|
||||
Also, please don't use the package for production apps yet.
|
||||
## Supported versions
|
||||
|
||||
At the moment, `moor_ffi` supports iOS, macOS and Android out of the box. Most Linux
|
||||
Distros have sqlite available as a shared library, those are supported as well.
|
||||
|
@ -40,39 +36,69 @@ DynamicLibrary _openOnLinux() {
|
|||
|
||||
## Migrating from moor_flutter to moor_ffi
|
||||
|
||||
__Again__: If you're only looking for Android and iOS support, `moor_flutter` is the
|
||||
right library to use.
|
||||
If you're not running into a limitation that forces you to use `moor_ffi`, be aware
|
||||
that staying on `moor_flutter` is a more stable solution at the moment.
|
||||
|
||||
First, adapt your `pubspec.yaml`: You can remove the `moor_flutter` dependency and instead
|
||||
add both the `moor` and `moor_ffi` dependencies:
|
||||
```yaml
|
||||
dependencies:
|
||||
moor: ^2.0.0
|
||||
moor_ffi: ^0.2.0
|
||||
sqflite: ^1.1.7 # Still used to obtain the database location
|
||||
dev_dependencies:
|
||||
moor_generator: ^2.0.0
|
||||
```
|
||||
|
||||
Adapt your imports:
|
||||
|
||||
1. Adapt your `pubspec.yaml`: You can remove the `moor_flutter` dependency and instead
|
||||
add both the `moor` and `moor_ffi` dependencies:
|
||||
```yaml
|
||||
dependencies:
|
||||
moor: ^2.0.0
|
||||
moor_ffi: ^0.0.1
|
||||
dev_dependencies:
|
||||
moor_generator: ^2.0.0
|
||||
```
|
||||
Note: If you were using `FlutterQueryExecutor.inDatabasesFolder`, you should also depend
|
||||
on `path_provider`. For desktop support of that library, see [this readme](https://github.com/google/flutter-desktop-embedding/tree/master/plugins/flutter_plugins).
|
||||
2. Adapt your imports:
|
||||
- In the file where you created a `FlutterQueryExecutor`, replace the `moor_flutter` import
|
||||
with `package:moor_ffi/moor_ffi.dart`.
|
||||
- In all other files where you might have import `moor_flutter`, just import `package:moor/moor.dart`.
|
||||
3. Replace the executor. This code:
|
||||
```dart
|
||||
FlutterQueryExecutor.inDatabaseFolder(path: 'db.sqlite')
|
||||
```
|
||||
can now be written as
|
||||
```dart
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
Replace the executor. This code:
|
||||
```dart
|
||||
FlutterQueryExecutor.inDatabaseFolder(path: 'db.sqlite')
|
||||
```
|
||||
can now be written as
|
||||
```dart
|
||||
import 'package:sqflite/sqflite.dart' show getDatabasesPath;
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
LazyDatabase(() async {
|
||||
final dbFolder = await getApplicationDocumentsDirectory();
|
||||
final file = File(j.join(dbFolder.path, 'db.sqlite'));
|
||||
return VmDatabase(file);
|
||||
})
|
||||
```
|
||||
__Important warning__: `FlutterQueryExecutor.inDatabaseFolder` may use a different folder on Android, which
|
||||
can cause data loss. This documentation will provide a better migration guide once `moor_ffi` is stable.
|
||||
Please create an issue if you need guidance on this soon.
|
||||
LazyDatabase(() async {
|
||||
final dbFolder = await getDatabasesPath();
|
||||
final file = File(j.join(dbFolder, 'db.sqlite'));
|
||||
return VmDatabase(file);
|
||||
})
|
||||
```
|
||||
|
||||
Note: If you haven't shipped a version with `moor_flutter` to your users yet, you can drop the dependency
|
||||
on `sqflite`. Instead, you can use `path_provider` which [works on Desktop](https://github.com/google/flutter-desktop-embedding/tree/master/plugins/flutter_plugins).
|
||||
Please be aware that `FlutterQueryExecutor.inDatabaseFolder` might yield a different folder than
|
||||
`path_provider` on Android. This can cause data loss if you've already shipped a version using
|
||||
`moor_flutter`. In that case, using `getDatabasePath` from sqflite is the suggested solution.
|
||||
|
||||
## Used compile options on Android
|
||||
|
||||
Note: Android is the only platform where moor_ffi will compile sqlite. The sqlite3 library from the system
|
||||
is used on all other platforms. The choosen options help reduce binary size by removing features not used by
|
||||
moor. Important options are marked in bold.
|
||||
|
||||
- We use the `-O3` performance option
|
||||
- __SQLITE_DQS=0__: This will make sqlite not accept double-quoted strings (and instead parse them as identifiers). This matches
|
||||
the behavior of moor and compiled queries
|
||||
- __SQLITE_THREADSAFE=0__: Since the majority of Flutter apps only use one isolate, thread safety is turned off. Note that you
|
||||
can still use the [isolate api]({{<relref "../Advanced Features/isolates.md">}}) for background operations. As long as all
|
||||
database accesses happen from the same thread, there's no problem.
|
||||
- SQLITE_DEFAULT_MEMSTATUS=0: The `sqlite3_status()` interfaces are not exposed by moor_ffi, so there's no point of having them.
|
||||
- SQLITE_MAX_EXPR_DEPTH=0: Disables maximum depth when sqlite parses expressions, which can make the parser faster.
|
||||
- `SQLITE_OMIT_AUTHORIZATION`, `SQLITE_OMIT_DECLTYPE`, __SQLITE_OMIT_DEPRECATED__, `SQLITE_OMIT_GET_TABLE`, `SQLITE_OMIT_LOAD_EXTENSION`,
|
||||
`SQLITE_OMIT_PROGRESS_CALLBACK`, `SQLITE_OMIT_SHARED_CACHE`, `SQLITE_OMIT_TCL_VARIABLE`, `SQLITE_OMIT_TRACE`: Disables features not supported
|
||||
by moor.
|
||||
- `SQLITE_USE_ALLOCA`: Allocate temporary memory on the stack
|
||||
- `SQLITE_UNTESTABLE`: Remove util functions that are only required to test sqlite3
|
||||
- `SQLITE_HAVE_ISNAN`: Use the `isnan` function from the system instead of the one shipped with sqlite3.
|
||||
- `SQLITE_ENABLE_FTS5`: Enable the [fts5](https://www.sqlite.org/fts5.html) engine for full-text search.
|
||||
- `SQLITE_ENABLE_JSON1`: Enable the [json1](https://www.sqlite.org/json1.html) extension for json support in sql query.
|
||||
|
||||
For more details on sqlite compile options, see [their documentation](https://www.sqlite.org/compile.html).
|
|
@ -0,0 +1,70 @@
|
|||
---
|
||||
title: "Supported sqlite extensions"
|
||||
weight: 10
|
||||
description: Information on json1 and fts5 support in the generator
|
||||
---
|
||||
|
||||
_Note_: Since `moor_flutter` uses the sqlite version shipped on the device, these extensions might not
|
||||
be available on all devices. When using these extensions, using `moor_ffi` is strongly recommended.
|
||||
This enables the extensions listed here on all Android devices and on iOS 11 and later.
|
||||
|
||||
## json1
|
||||
|
||||
To enable the json1 extension in moor files and compiled queries, modify your
|
||||
[build options]({{<relref "../Advanced Features/builder_options.md">}}) to include
|
||||
`json1` in the `sqlite_module` section.
|
||||
|
||||
The sqlite extension doesn't require any special tables and works on all text columns. In moor
|
||||
files and compiled queries, all `json` functions are available after enabling the extension.
|
||||
|
||||
Since the json extension is optional, enabling it in Dart requires a special import,
|
||||
`package:moor/extensions/json1.dart`. An example that uses json functions in Dart is shown below:
|
||||
```dart
|
||||
import 'package:moor/moor.dart';
|
||||
import 'package:moor/extensions/json1.dart';
|
||||
|
||||
class Contacts extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get data => text()();
|
||||
}
|
||||
|
||||
@UseMoor(tables: [Contacts])
|
||||
class Database extends _$Database {
|
||||
// constructor and schemaVersion omitted for brevity
|
||||
|
||||
Future<List<Contacts>> findContactsWithNumber(String number) {
|
||||
return (select(contacts)
|
||||
..where((row) {
|
||||
// assume the phone number is stored in a json key in the `data` column
|
||||
final phoneNumber = row.data.jsonExtract<String, StringType>('phone_number');
|
||||
return phoneNumber.equals(number);
|
||||
})
|
||||
).get();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can learn more about the json1 extension on [sqlite.org](https://www.sqlite.org/json1.html).
|
||||
|
||||
## fts5
|
||||
|
||||
The fts5 extension provides full-text search capabilities in sqlite tables.
|
||||
To enable the fts5 extension in moor files and compiled queries, modify the
|
||||
[build options]({{<relref "../Advanced Features/builder_options.md">}}) to include
|
||||
`fts5` in the `sqlite_module` section.
|
||||
|
||||
Just like you'd expect when using sqlite, you can create a fts5 table in a moor file
|
||||
by using a `CREATE VIRTUAL TABLE` statement.
|
||||
```sql
|
||||
CREATE VIRTUAL TABLE email USING fts5(sender, title, body);
|
||||
```
|
||||
|
||||
Queries on fts5 tables work like expected:
|
||||
```sql
|
||||
emailsWithFts5: SELECT * FROM email WHERE email MATCH 'fts5' ORDER BY rank;
|
||||
```
|
||||
|
||||
The `bm25`, `highlight` and `snippet` functions from fts5 can also be used in custom queries.
|
||||
|
||||
It's not possible to declare fts5 tables, or queries on fts5 tables, in Dart.
|
||||
You can learn more about the fts5 extension on [sqlite.org](https://www.sqlite.org/fts5.html).
|
|
@ -78,6 +78,13 @@ with @ or $. The compiler will attempt to infer the variable's type by
|
|||
looking at its context. This lets moor generate typesafe apis for your
|
||||
queries, the variables will be written as parameters to your method.
|
||||
|
||||
When it's ambigous, the analyzer might be unable to resolve the type of
|
||||
a variable. For those scenarios, you can also denote the explicit type
|
||||
of a variable:
|
||||
```sql
|
||||
myQuery(:variable AS TEXT): SELECT :variable;
|
||||
```
|
||||
|
||||
### Arrays
|
||||
If you want to check whether a value is in an array of values, you can
|
||||
use `IN ?`. That's not valid sql, but moor will desugar that at runtime. So, for this query:
|
||||
|
@ -119,9 +126,13 @@ You can put import statements at the top of a `moor` file:
|
|||
import 'other.moor'; -- single quotes are required for imports
|
||||
```
|
||||
All tables reachable from the other file will then also be visible in
|
||||
the current file and to the database that `includes` it. Importing
|
||||
Dart files into a moor file will also work - then, all the tables
|
||||
declared via Dart tables can be used inside queries.
|
||||
the current file and to the database that `includes` it. If you want
|
||||
to declare queries on tables that were defined in another moor
|
||||
file, you also need to import that file for the tables to be
|
||||
visible.
|
||||
|
||||
Importing Dart files into a moor file will also work - then,
|
||||
all the tables declared via Dart tables can be used inside queries.
|
||||
We support both relative imports and the `package:` imports you
|
||||
know from Dart.
|
||||
|
||||
|
|
|
@ -43,11 +43,9 @@ Make sure that your project depends on moor 2.0 or later. Then
|
|||
set to true. Contrary to its name, that flag turns on the plugin system, so you
|
||||
don't need to worry about angular.
|
||||
2. Tell Dart Code to analyze moor files as well. Add this to your `settings.json`:
|
||||
```json
|
||||
"dart.additionalAnalyzerFileExtensions": [
|
||||
"moor"
|
||||
]
|
||||
```
|
||||
```json
|
||||
"dart.additionalAnalyzerFileExtensions": ["moor"]
|
||||
```
|
||||
3. Finally, close and reopen your IDE so that the analysis server is restarted. The analysis server will
|
||||
then load the moor plugin and start providing analysis results for `.moor` files. Loading the plugin
|
||||
can take some time (around a minute for the first time).
|
||||
|
|
|
@ -41,6 +41,26 @@ If you're strict on keeping your business logic out of the widget layer, you pro
|
|||
framework like `kiwi` or `get_it` to instantiate services and view models. Creating a singleton instance of `MyDatabase`
|
||||
in your favorite dependency injection framework for flutter hence solves this problem for you.
|
||||
|
||||
## Why am I getting no such table errors?
|
||||
|
||||
If you add another table after your app has already been installed, you need to write a [migration]({{< relref "Advanced Features/migrations.md" >}})
|
||||
that covers creating that table. If you're in the process of developing your app and want to use un- and reinstall your app
|
||||
instead of writing migrations, that's fine too. Please note that your apps data might be backed up on Android, so
|
||||
manually deleting your app's data instead of a reinstall is necessary on some devices.
|
||||
|
||||
## How do I fix lints in generated files?
|
||||
|
||||
Based on your linter settings, you might see some warnings in the `.g.dart` files generated by moor. Since lints are mainly used for
|
||||
human-written code, we recommend to disable static analysis on generated files. For that, generate a top-level file called
|
||||
`analysis_options.yaml` in your project and add this content:
|
||||
```yaml
|
||||
analyzer:
|
||||
exclude:
|
||||
- "**/*.g.dart"
|
||||
```
|
||||
|
||||
You might have to restart your IDE for the changes to apply.
|
||||
|
||||
## How does moor compare to X?
|
||||
There are a variety of good persistence libraries for Dart and Flutter.
|
||||
|
||||
|
|
|
@ -52,6 +52,17 @@ class MyDatabase extends _$MyDatabase {
|
|||
}
|
||||
```
|
||||
|
||||
{{% alert title="Installing sqlite" %}}
|
||||
We can't distribute an sqlite installation as a pub package (at least
|
||||
not as something that works outside of a Flutter build), so you need
|
||||
to ensure that you have the sqlite3 shared library installed on your
|
||||
system. On macOS, it's installed by default. On Linux, you can use the
|
||||
`libsqlite3-dev` package on Ubuntu and the `sqlite3` package on Arch
|
||||
(other distros will have similar packages). I'm not sure how it works
|
||||
on Windows, but [downloading sqlite](https://www.sqlite.org/download.html)
|
||||
and extracting `sqlite3.dll` into your application folder might work.
|
||||
{{% /alert %}}
|
||||
|
||||
## Writing tests
|
||||
|
||||
We can create an in-memory version of the database by using a
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
<div class="alert alert-primary" role="alert">
|
||||
<h4 class="alert-heading">Change to moor_ffi</h4>
|
||||
Previous versions of this article recommended to use <code>moor_flutter</code>.
|
||||
New users are recommended to use the <code>moor_ffi</code> package instead.
|
||||
If you're already using <code>moor_flutter</code>, there's nothing to worry about!
|
||||
The package is still maintained and will continue to work.
|
||||
</div>
|
|
@ -0,0 +1,27 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:benchmarks/benchmarks.dart';
|
||||
|
||||
final File output = File('benchmark_results.json');
|
||||
|
||||
Future<void> main() async {
|
||||
final tracker = TrackingEmitter();
|
||||
ComparingEmitter comparer;
|
||||
if (await output.exists()) {
|
||||
final content = json.decode(await output.readAsString());
|
||||
final oldData = (content as Map).cast<String, double>();
|
||||
comparer = ComparingEmitter(oldData);
|
||||
} else {
|
||||
comparer = ComparingEmitter();
|
||||
}
|
||||
|
||||
final emitter = MultiEmitter([tracker, comparer]);
|
||||
final benchmarks = allBenchmarks(emitter);
|
||||
|
||||
for (final benchmark in benchmarks) {
|
||||
await benchmark.report();
|
||||
}
|
||||
|
||||
output.writeAsStringSync(json.encode(tracker.timings));
|
||||
}
|
|
@ -0,0 +1,115 @@
|
|||
part of 'benchmarks.dart';
|
||||
|
||||
// Some parts copied from https://github.com/dart-lang/benchmark_harness
|
||||
// Copyright 2011 Google Inc. All Rights Reserved.
|
||||
|
||||
// Forked to create the async counterpart and the common Reportable class
|
||||
|
||||
abstract class Reportable {
|
||||
FutureOr<void> report();
|
||||
}
|
||||
|
||||
abstract class BenchmarkBase implements Reportable {
|
||||
final String name;
|
||||
final ScoreEmitter emitter;
|
||||
|
||||
const BenchmarkBase(this.name, this.emitter);
|
||||
|
||||
void run();
|
||||
|
||||
void warmup() {
|
||||
run();
|
||||
}
|
||||
|
||||
void exercise() {
|
||||
for (var i = 0; i < 10; i++) {
|
||||
run();
|
||||
}
|
||||
}
|
||||
|
||||
void setup() {}
|
||||
|
||||
void teardown() {}
|
||||
|
||||
static double measureFor(Function f, int minimumMillis) {
|
||||
final minimumMicros = minimumMillis * 1000;
|
||||
var iter = 0;
|
||||
final watch = Stopwatch();
|
||||
watch.start();
|
||||
var elapsed = 0;
|
||||
while (elapsed < minimumMicros) {
|
||||
f();
|
||||
elapsed = watch.elapsedMicroseconds;
|
||||
iter++;
|
||||
}
|
||||
return elapsed / iter;
|
||||
}
|
||||
|
||||
double measure() {
|
||||
setup();
|
||||
// Warmup for at least 100ms. Discard result.
|
||||
measureFor(warmup, 100);
|
||||
// Run the benchmark for at least 2000ms.
|
||||
final result = measureFor(exercise, 2000);
|
||||
teardown();
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
void report() {
|
||||
emitter.emit(name, measure());
|
||||
}
|
||||
}
|
||||
|
||||
abstract class AsyncBenchmarkBase implements Reportable {
|
||||
final String name;
|
||||
final ScoreEmitter emitter;
|
||||
|
||||
const AsyncBenchmarkBase(this.name, this.emitter);
|
||||
|
||||
Future<void> run();
|
||||
|
||||
Future<void> warmup() {
|
||||
return run();
|
||||
}
|
||||
|
||||
Future<void> exercise() {
|
||||
return run();
|
||||
}
|
||||
|
||||
Future<void> setup() async {}
|
||||
|
||||
Future<void> teardown() async {}
|
||||
|
||||
static Future<double> measureFor(
|
||||
Future Function() f, int minimumMillis) async {
|
||||
final minimumMicros = minimumMillis * 1000;
|
||||
var iter = 0;
|
||||
final watch = Stopwatch();
|
||||
watch.start();
|
||||
var elapsed = 0;
|
||||
while (elapsed < minimumMicros) {
|
||||
await f();
|
||||
elapsed = watch.elapsedMicroseconds;
|
||||
iter++;
|
||||
}
|
||||
return elapsed / iter;
|
||||
}
|
||||
|
||||
Future<double> measure() async {
|
||||
await setup();
|
||||
try {
|
||||
// Warmup for at least 100ms. Discard result.
|
||||
await measureFor(warmup, 100);
|
||||
// Run the benchmark for at least 2000ms.
|
||||
return await measureFor(exercise, 2000);
|
||||
} finally {
|
||||
await teardown();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> report() async {
|
||||
emitter.emit(name, await measure());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:benchmark_harness/benchmark_harness.dart' show ScoreEmitter;
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import 'src/moor/key_value_insert.dart';
|
||||
import 'src/sqlite/bind_string.dart';
|
||||
import 'src/sqlparser/parse_moor_file.dart';
|
||||
import 'src/sqlparser/tokenizer.dart';
|
||||
|
||||
export 'package:benchmark_harness/benchmark_harness.dart' show ScoreEmitter;
|
||||
|
||||
part 'benchmark_base.dart';
|
||||
|
||||
List<Reportable> allBenchmarks(ScoreEmitter emitter) {
|
||||
return [
|
||||
// low-level sqlite native interop
|
||||
SelectStringBenchmark(emitter),
|
||||
// high-level moor apis
|
||||
KeyValueInsertBatch(emitter),
|
||||
KeyValueInsertSerial(emitter),
|
||||
// sql parser
|
||||
ParseMoorFile(emitter),
|
||||
TokenizerBenchmark(emitter),
|
||||
];
|
||||
}
|
||||
|
||||
class TrackingEmitter implements ScoreEmitter {
|
||||
/// The average time it took to run each benchmark, in microseconds.
|
||||
final Map<String, double> timings = {};
|
||||
|
||||
@override
|
||||
void emit(String testName, double value) {
|
||||
timings[testName] = value;
|
||||
}
|
||||
}
|
||||
|
||||
class ComparingEmitter implements ScoreEmitter {
|
||||
final Map<String, double> oldTimings;
|
||||
|
||||
static final _percent = NumberFormat('##.##%');
|
||||
|
||||
ComparingEmitter([this.oldTimings = const {}]);
|
||||
|
||||
@override
|
||||
void emit(String testName, double value) {
|
||||
final content = StringBuffer(testName)
|
||||
..write(': ')
|
||||
..write(value)
|
||||
..write(' us');
|
||||
|
||||
if (oldTimings.containsKey(testName)) {
|
||||
final oldTime = oldTimings[testName];
|
||||
final increasedTime = value - oldTime;
|
||||
|
||||
final relative = increasedTime.abs() / oldTime;
|
||||
|
||||
content.write('; delta: ');
|
||||
if (increasedTime < 0) {
|
||||
content
|
||||
..write('$increasedTime us, -')
|
||||
..write(_percent.format(relative));
|
||||
} else {
|
||||
content
|
||||
..write('+$increasedTime us, +')
|
||||
..write(_percent.format(relative));
|
||||
}
|
||||
}
|
||||
|
||||
print(content);
|
||||
}
|
||||
}
|
||||
|
||||
class MultiEmitter implements ScoreEmitter {
|
||||
final List<ScoreEmitter> delegates;
|
||||
|
||||
const MultiEmitter(this.delegates);
|
||||
|
||||
@override
|
||||
void emit(String testName, double value) {
|
||||
for (final delegate in delegates) {
|
||||
delegate.emit(testName, value);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:moor/moor.dart';
|
||||
import 'package:moor_ffi/moor_ffi.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
part 'database.g.dart';
|
||||
|
||||
class KeyValues extends Table {
|
||||
TextColumn get key => text()();
|
||||
TextColumn get value => text()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {key};
|
||||
}
|
||||
|
||||
@UseMoor(tables: [KeyValues])
|
||||
class Database extends _$Database {
|
||||
Database() : super(_obtainExecutor());
|
||||
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
}
|
||||
|
||||
final _uuid = Uuid();
|
||||
|
||||
QueryExecutor _obtainExecutor() {
|
||||
final file =
|
||||
File(p.join(Directory.systemTemp.path, 'moor_benchmarks', _uuid.v4()));
|
||||
file.parent.createSync();
|
||||
|
||||
return VmDatabase(file);
|
||||
}
|
|
@ -0,0 +1,177 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'database.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// MoorGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// ignore_for_file: unnecessary_brace_in_string_interps, unnecessary_this
|
||||
class KeyValue extends DataClass implements Insertable<KeyValue> {
|
||||
final String key;
|
||||
final String value;
|
||||
KeyValue({@required this.key, @required this.value});
|
||||
factory KeyValue.fromData(Map<String, dynamic> data, GeneratedDatabase db,
|
||||
{String prefix}) {
|
||||
final effectivePrefix = prefix ?? '';
|
||||
final stringType = db.typeSystem.forDartType<String>();
|
||||
return KeyValue(
|
||||
key: stringType.mapFromDatabaseResponse(data['${effectivePrefix}key']),
|
||||
value:
|
||||
stringType.mapFromDatabaseResponse(data['${effectivePrefix}value']),
|
||||
);
|
||||
}
|
||||
factory KeyValue.fromJson(Map<String, dynamic> json,
|
||||
{ValueSerializer serializer = const ValueSerializer.defaults()}) {
|
||||
return KeyValue(
|
||||
key: serializer.fromJson<String>(json['key']),
|
||||
value: serializer.fromJson<String>(json['value']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson(
|
||||
{ValueSerializer serializer = const ValueSerializer.defaults()}) {
|
||||
return <String, dynamic>{
|
||||
'key': serializer.toJson<String>(key),
|
||||
'value': serializer.toJson<String>(value),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
KeyValuesCompanion createCompanion(bool nullToAbsent) {
|
||||
return KeyValuesCompanion(
|
||||
key: key == null && nullToAbsent ? const Value.absent() : Value(key),
|
||||
value:
|
||||
value == null && nullToAbsent ? const Value.absent() : Value(value),
|
||||
);
|
||||
}
|
||||
|
||||
KeyValue copyWith({String key, String value}) => KeyValue(
|
||||
key: key ?? this.key,
|
||||
value: value ?? this.value,
|
||||
);
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('KeyValue(')
|
||||
..write('key: $key, ')
|
||||
..write('value: $value')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => $mrjf($mrjc(key.hashCode, value.hashCode));
|
||||
@override
|
||||
bool operator ==(dynamic other) =>
|
||||
identical(this, other) ||
|
||||
(other is KeyValue && other.key == this.key && other.value == this.value);
|
||||
}
|
||||
|
||||
class KeyValuesCompanion extends UpdateCompanion<KeyValue> {
|
||||
final Value<String> key;
|
||||
final Value<String> value;
|
||||
const KeyValuesCompanion({
|
||||
this.key = const Value.absent(),
|
||||
this.value = const Value.absent(),
|
||||
});
|
||||
KeyValuesCompanion.insert({
|
||||
@required String key,
|
||||
@required String value,
|
||||
}) : key = Value(key),
|
||||
value = Value(value);
|
||||
KeyValuesCompanion copyWith({Value<String> key, Value<String> value}) {
|
||||
return KeyValuesCompanion(
|
||||
key: key ?? this.key,
|
||||
value: value ?? this.value,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class $KeyValuesTable extends KeyValues
|
||||
with TableInfo<$KeyValuesTable, KeyValue> {
|
||||
final GeneratedDatabase _db;
|
||||
final String _alias;
|
||||
$KeyValuesTable(this._db, [this._alias]);
|
||||
final VerificationMeta _keyMeta = const VerificationMeta('key');
|
||||
GeneratedTextColumn _key;
|
||||
@override
|
||||
GeneratedTextColumn get key => _key ??= _constructKey();
|
||||
GeneratedTextColumn _constructKey() {
|
||||
return GeneratedTextColumn(
|
||||
'key',
|
||||
$tableName,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
final VerificationMeta _valueMeta = const VerificationMeta('value');
|
||||
GeneratedTextColumn _value;
|
||||
@override
|
||||
GeneratedTextColumn get value => _value ??= _constructValue();
|
||||
GeneratedTextColumn _constructValue() {
|
||||
return GeneratedTextColumn(
|
||||
'value',
|
||||
$tableName,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<GeneratedColumn> get $columns => [key, value];
|
||||
@override
|
||||
$KeyValuesTable get asDslTable => this;
|
||||
@override
|
||||
String get $tableName => _alias ?? 'key_values';
|
||||
@override
|
||||
final String actualTableName = 'key_values';
|
||||
@override
|
||||
VerificationContext validateIntegrity(KeyValuesCompanion d,
|
||||
{bool isInserting = false}) {
|
||||
final context = VerificationContext();
|
||||
if (d.key.present) {
|
||||
context.handle(_keyMeta, key.isAcceptableValue(d.key.value, _keyMeta));
|
||||
} else if (key.isRequired && isInserting) {
|
||||
context.missing(_keyMeta);
|
||||
}
|
||||
if (d.value.present) {
|
||||
context.handle(
|
||||
_valueMeta, value.isAcceptableValue(d.value.value, _valueMeta));
|
||||
} else if (value.isRequired && isInserting) {
|
||||
context.missing(_valueMeta);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {key};
|
||||
@override
|
||||
KeyValue map(Map<String, dynamic> data, {String tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : null;
|
||||
return KeyValue.fromData(data, _db, prefix: effectivePrefix);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Variable> entityToSql(KeyValuesCompanion d) {
|
||||
final map = <String, Variable>{};
|
||||
if (d.key.present) {
|
||||
map['key'] = Variable<String, StringType>(d.key.value);
|
||||
}
|
||||
if (d.value.present) {
|
||||
map['value'] = Variable<String, StringType>(d.value.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
$KeyValuesTable createAlias(String alias) {
|
||||
return $KeyValuesTable(_db, alias);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _$Database extends GeneratedDatabase {
|
||||
_$Database(QueryExecutor e) : super(SqlTypeSystem.defaultInstance, e);
|
||||
$KeyValuesTable _keyValues;
|
||||
$KeyValuesTable get keyValues => _keyValues ??= $KeyValuesTable(this);
|
||||
@override
|
||||
List<TableInfo> get allTables => [keyValues];
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
import 'package:benchmarks/benchmarks.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import 'database.dart';
|
||||
|
||||
// ignore_for_file: invalid_use_of_visible_for_testing_member
|
||||
// ignore_for_file: invalid_use_of_protected_member
|
||||
|
||||
const int _size = 1000;
|
||||
|
||||
class KeyValueInsertBatch extends AsyncBenchmarkBase {
|
||||
final _db = Database();
|
||||
final Uuid uuid = Uuid();
|
||||
|
||||
KeyValueInsertBatch(ScoreEmitter emitter)
|
||||
: super('Inserting $_size entries (batch)', emitter);
|
||||
|
||||
@override
|
||||
Future<void> run() async {
|
||||
await _db.delete(_db.keyValues).go();
|
||||
|
||||
await _db.batch((batch) {
|
||||
for (var i = 0; i < _size; i++) {
|
||||
final key = uuid.v4();
|
||||
final value = uuid.v4();
|
||||
|
||||
batch.insert(
|
||||
_db.keyValues, KeyValuesCompanion.insert(key: key, value: value));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class KeyValueInsertSerial extends AsyncBenchmarkBase {
|
||||
final _db = Database();
|
||||
final Uuid uuid = Uuid();
|
||||
|
||||
KeyValueInsertSerial(ScoreEmitter emitter)
|
||||
: super('Inserting $_size entries (serial)', emitter);
|
||||
|
||||
@override
|
||||
Future<void> run() async {
|
||||
await _db.delete(_db.keyValues).go();
|
||||
|
||||
for (var i = 0; i < _size; i++) {
|
||||
final key = uuid.v4();
|
||||
final value = uuid.v4();
|
||||
|
||||
await _db
|
||||
.into(_db.keyValues)
|
||||
.insert(KeyValuesCompanion.insert(key: key, value: value));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
import 'package:benchmarks/benchmarks.dart';
|
||||
import 'package:moor_ffi/database.dart';
|
||||
|
||||
class SelectStringBenchmark extends BenchmarkBase {
|
||||
SelectStringBenchmark(ScoreEmitter emitter)
|
||||
: super('SELECTing a single string variable', emitter);
|
||||
|
||||
PreparedStatement statement;
|
||||
Database database;
|
||||
|
||||
@override
|
||||
void setup() {
|
||||
database = Database.memory();
|
||||
statement = database.prepare('SELECT ?;');
|
||||
}
|
||||
|
||||
@override
|
||||
void run() {
|
||||
statement.select(const ['hello sqlite, can you return this string?']);
|
||||
}
|
||||
|
||||
@override
|
||||
void exercise() {
|
||||
// repeat 1000 instead of 10 times to reduce variance
|
||||
for (var i = 0; i < 1000; i++) {
|
||||
run();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void teardown() {
|
||||
statement.close();
|
||||
database.close();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
import 'package:benchmarks/benchmarks.dart';
|
||||
import 'package:sqlparser/sqlparser.dart';
|
||||
|
||||
const file = '''
|
||||
|
||||
foo: SELECT
|
||||
l_orderkey,
|
||||
SUM(l_extendedprice * (1 - l_discount)) AS revenue,
|
||||
o_orderdate,
|
||||
o_shippriority
|
||||
FROM
|
||||
customer,
|
||||
orders,
|
||||
lineitem
|
||||
WHERE
|
||||
c_mktsegment = '%s'
|
||||
and c_custkey = o_custkey
|
||||
and l_orderkey = o_orderkey
|
||||
and o_orderdate < '%s'
|
||||
and l_shipdate > '%s'
|
||||
GROUP BY
|
||||
l_orderkey,
|
||||
o_orderdate,
|
||||
o_shippriority
|
||||
ORDER BY
|
||||
revenue DESC,
|
||||
o_orderdate;
|
||||
|
||||
manyColumns:
|
||||
SELECT a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z FROM test;
|
||||
''';
|
||||
|
||||
class ParseMoorFile extends BenchmarkBase {
|
||||
ParseMoorFile(ScoreEmitter emitter) : super('Moor file: Parse only', emitter);
|
||||
|
||||
final _engine = SqlEngine(useMoorExtensions: true);
|
||||
|
||||
@override
|
||||
void exercise() {
|
||||
for (var i = 0; i < 10; i++) {
|
||||
assert(_engine.parseMoorFile(file).errors.isEmpty);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void run() {
|
||||
_engine.parseMoorFile(file);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:benchmarks/benchmarks.dart';
|
||||
// ignore: implementation_imports
|
||||
import 'package:sqlparser/src/reader/tokenizer/token.dart';
|
||||
// ignore: implementation_imports
|
||||
import 'package:sqlparser/src/reader/tokenizer/scanner.dart';
|
||||
|
||||
class TokenizerBenchmark extends BenchmarkBase {
|
||||
StringBuffer input;
|
||||
|
||||
static const int size = 10000;
|
||||
|
||||
TokenizerBenchmark(ScoreEmitter emitter)
|
||||
: super('Tokenizing $size keywords', emitter);
|
||||
|
||||
@override
|
||||
void setup() {
|
||||
input = StringBuffer();
|
||||
|
||||
final random = Random();
|
||||
final keywordLexemes = keywords.keys.toList();
|
||||
for (var i = 0; i < size; i++) {
|
||||
final keyword = keywordLexemes[random.nextInt(keywordLexemes.length)];
|
||||
input..write(' ')..write(keyword);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void run() {
|
||||
final scanner = Scanner(input.toString());
|
||||
scanner.scanTokens();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
name: benchmarks
|
||||
description: Runs simple and complex benchmarks to measure performance of moor and moor_ffi
|
||||
|
||||
dependencies:
|
||||
moor:
|
||||
moor_ffi:
|
||||
benchmark_harness: ^1.0.5
|
||||
intl: ^0.16.0
|
||||
uuid: ^2.0.0
|
||||
path: ^1.6.0
|
||||
dev_dependencies:
|
||||
moor_generator:
|
||||
build_runner:
|
||||
test:
|
||||
|
||||
dependency_overrides:
|
||||
moor:
|
||||
path: ../../moor
|
||||
moor_ffi:
|
||||
path: ../../moor_ffi
|
||||
moor_generator:
|
||||
path: ../../moor_generator
|
||||
sqlparser:
|
||||
path: ../../sqlparser
|
|
@ -0,0 +1,37 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:benchmarks/benchmarks.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
test('compares time for increase', () {
|
||||
final comparer = ComparingEmitter({'foo': 10.0});
|
||||
final output = printsOf(() => comparer.emit('foo', 12.5));
|
||||
expect(output, ['foo: 12.5 us; delta: +2.5 us, +25%']);
|
||||
});
|
||||
|
||||
test('compares time for decrease', () {
|
||||
final comparer = ComparingEmitter({'foo': 10.0});
|
||||
final output = printsOf(() => comparer.emit('foo', 7.5));
|
||||
expect(output, ['foo: 7.5 us; delta: -2.5 us, -25%']);
|
||||
});
|
||||
|
||||
test('no comparison when old value unknown', () {
|
||||
final comparer = ComparingEmitter();
|
||||
final output = printsOf(() => comparer.emit('foo', 10));
|
||||
expect(output, ['foo: 10.0 us']);
|
||||
});
|
||||
}
|
||||
|
||||
List<String> printsOf(Function() code) {
|
||||
final output = <String>[];
|
||||
|
||||
runZoned(
|
||||
code,
|
||||
zoneSpecification: ZoneSpecification(
|
||||
print: (_, __, ___, line) => output.add(line),
|
||||
),
|
||||
);
|
||||
|
||||
return output;
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
// Adapted from https://github.com/dart-lang/coverage/blob/master/bin/format_coverage.dart
|
||||
// because that file doesn't work with the output of the test package.
|
||||
|
||||
// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:coverage/coverage.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
// ignore_for_file: avoid_print
|
||||
|
||||
final File outputFile = File('lcov.info');
|
||||
|
||||
Future<void> main() async {
|
||||
if (outputFile.existsSync()) {
|
||||
outputFile.deleteSync();
|
||||
}
|
||||
|
||||
await runForProject('moor');
|
||||
await runForProject('sqlparser');
|
||||
}
|
||||
|
||||
Future runForProject(String projectName) async {
|
||||
final files = filesToProcess(projectName);
|
||||
print('$projectName: Collecting across ${files.length} files');
|
||||
|
||||
final hitmap = await parseCoverage(files, 1);
|
||||
|
||||
final resolver = Resolver(packagesPath: p.join(projectName, '.packages'));
|
||||
|
||||
final output =
|
||||
await LcovFormatter(resolver, reportOn: [p.join(projectName, 'lib')])
|
||||
.format(hitmap);
|
||||
|
||||
await outputFile.writeAsString(output, mode: FileMode.append);
|
||||
}
|
||||
|
||||
List<File> filesToProcess(String moorSubproject) {
|
||||
final filePattern = RegExp(r'^.*\.json$');
|
||||
final coverageOutput = p.join(moorSubproject, 'coverage', 'test');
|
||||
|
||||
if (FileSystemEntity.isDirectorySync(coverageOutput)) {
|
||||
return Directory(coverageOutput)
|
||||
.listSync(recursive: true)
|
||||
.whereType<File>()
|
||||
.where((e) => filePattern.hasMatch(p.basename(e.path)))
|
||||
.toList();
|
||||
}
|
||||
throw AssertionError('Moor subproject at $moorSubproject does not exist');
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
name: coverage_formatting
|
||||
publish_to: none
|
||||
description: Tool used for the CI to format VM coverage jsons to LCOV files
|
||||
|
||||
dependencies:
|
||||
coverage: '>=0.13.3'
|
||||
path: ^1.6.4
|
|
@ -115,8 +115,8 @@ mixin _SqfliteExecutor on QueryDelegate {
|
|||
Future<void> runBatched(List<BatchedStatement> statements) async {
|
||||
final batch = db.batch();
|
||||
|
||||
for (var statement in statements) {
|
||||
for (var boundVariables in statement.variables) {
|
||||
for (final statement in statements) {
|
||||
for (final boundVariables in statement.variables) {
|
||||
batch.execute(statement.sql, boundVariables);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
org.gradle.jvmargs=-Xmx1536M
|
||||
|
||||
android.enableR8=true
|
||||
|
|
|
@ -26,7 +26,7 @@ class FfiExecutor extends TestExecutor {
|
|||
}
|
||||
}
|
||||
|
||||
void main() async {
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
final dbPath = await getDatabasesPath();
|
||||
|
|
|
@ -28,7 +28,7 @@ class SqfliteExecutor extends TestExecutor {
|
|||
}
|
||||
}
|
||||
|
||||
void main() async {
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
runAllTests(SqfliteExecutor());
|
||||
|
|
|
@ -64,6 +64,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.16.1"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ffi
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.3"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
|
@ -152,7 +159,7 @@ packages:
|
|||
name: meta
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.7"
|
||||
version: "1.1.8"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -166,21 +173,21 @@ packages:
|
|||
path: "../../../moor"
|
||||
relative: true
|
||||
source: path
|
||||
version: "1.7.2"
|
||||
version: "2.1.0"
|
||||
moor_ffi:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "../../../moor_ffi"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.0.1"
|
||||
version: "0.2.0"
|
||||
moor_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "../../../moor_flutter"
|
||||
relative: true
|
||||
source: path
|
||||
version: "1.7.0"
|
||||
version: "2.0.0"
|
||||
multi_server_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -404,5 +411,5 @@ packages:
|
|||
source: hosted
|
||||
version: "2.2.0"
|
||||
sdks:
|
||||
dart: ">=2.5.0-dev <=2.6.0-dev.1.0.flutter-7c1821c4aa"
|
||||
dart: ">=2.6.0 <3.0.0"
|
||||
flutter: ">=1.2.1 <2.0.0"
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
analyzer:
|
||||
strong-mode:
|
||||
implicit-casts: false
|
||||
implicit-dynamic: false
|
|
@ -1,27 +1,25 @@
|
|||
import 'package:moor/moor.dart';
|
||||
import 'package:tests/database/database.dart';
|
||||
|
||||
class People {
|
||||
static const int dashId = 1, dukeId = 2, gopherId = 3;
|
||||
const int dashId = 1, dukeId = 2, gopherId = 3;
|
||||
|
||||
static UsersCompanion dash = UsersCompanion(
|
||||
name: const Value('Dash'),
|
||||
birthDate: Value(DateTime(2011, 10, 11)),
|
||||
);
|
||||
UsersCompanion dash = UsersCompanion(
|
||||
name: const Value('Dash'),
|
||||
birthDate: Value(DateTime(2011, 10, 11)),
|
||||
);
|
||||
|
||||
static UsersCompanion duke = UsersCompanion(
|
||||
name: const Value('Duke'),
|
||||
birthDate: Value(DateTime(1996, 1, 23)),
|
||||
);
|
||||
UsersCompanion duke = UsersCompanion(
|
||||
name: const Value('Duke'),
|
||||
birthDate: Value(DateTime(1996, 1, 23)),
|
||||
);
|
||||
|
||||
static UsersCompanion gopher = UsersCompanion(
|
||||
name: const Value('Go Gopher'),
|
||||
birthDate: Value(DateTime(2012, 3, 28)),
|
||||
);
|
||||
UsersCompanion gopher = UsersCompanion(
|
||||
name: const Value('Go Gopher'),
|
||||
birthDate: Value(DateTime(2012, 3, 28)),
|
||||
);
|
||||
|
||||
static UsersCompanion florian = UsersCompanion(
|
||||
name: const Value(
|
||||
'Florian, the fluffy Ferret from Florida familiar with Flutter'),
|
||||
birthDate: Value(DateTime(2015, 4, 29)),
|
||||
);
|
||||
}
|
||||
UsersCompanion florian = UsersCompanion(
|
||||
name: const Value(
|
||||
'Florian, the fluffy Ferret from Florida familiar with Flutter'),
|
||||
birthDate: Value(DateTime(2015, 4, 29)),
|
||||
);
|
||||
|
|
|
@ -3,7 +3,7 @@ import 'dart:convert';
|
|||
import 'package:json_annotation/json_annotation.dart' as j;
|
||||
import 'package:moor/moor.dart';
|
||||
|
||||
import 'package:tests/data/sample_data.dart';
|
||||
import 'package:tests/data/sample_data.dart' as people;
|
||||
|
||||
part 'database.g.dart';
|
||||
|
||||
|
@ -65,10 +65,12 @@ class PreferenceConverter extends TypeConverter<Preferences, String> {
|
|||
@UseMoor(
|
||||
tables: [Users, Friendships],
|
||||
queries: {
|
||||
'mostPopularUsers':
|
||||
'SELECT * FROM users u ORDER BY (SELECT COUNT(*) FROM friendships WHERE first_user = u.id OR second_user = u.id) DESC LIMIT :amount',
|
||||
'mostPopularUsers': 'SELECT * FROM users u '
|
||||
'ORDER BY (SELECT COUNT(*) FROM friendships '
|
||||
'WHERE first_user = u.id OR second_user = u.id) DESC LIMIT :amount',
|
||||
'amountOfGoodFriends':
|
||||
'SELECT COUNT(*) FROM friendships f WHERE f.really_good_friends AND (f.first_user = :user OR f.second_user = :user)',
|
||||
'SELECT COUNT(*) FROM friendships f WHERE f.really_good_friends AND '
|
||||
'(f.first_user = :user OR f.second_user = :user)',
|
||||
'friendsOf': '''SELECT u.* FROM friendships f
|
||||
INNER JOIN users u ON u.id IN (f.first_user, f.second_user) AND
|
||||
u.id != :user
|
||||
|
@ -101,8 +103,12 @@ class Database extends _$Database {
|
|||
},
|
||||
beforeOpen: (details) async {
|
||||
if (details.wasCreated) {
|
||||
await into(users)
|
||||
.insertAll([People.dash, People.duke, People.gopher]);
|
||||
// make sure that transactions can be used in the beforeOpen callback.
|
||||
await transaction(() async {
|
||||
batch((batch) {
|
||||
batch.insertAll(users, [people.dash, people.duke, people.gopher]);
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
@ -112,7 +118,7 @@ class Database extends _$Database {
|
|||
return transaction(() async {
|
||||
final id = user.id;
|
||||
await (delete(friendships)
|
||||
..where((f) => or(f.firstUser.equals(id), f.secondUser.equals(id))))
|
||||
..where((f) => f.firstUser.equals(id) | f.secondUser.equals(id)))
|
||||
.go();
|
||||
|
||||
if (fail) {
|
||||
|
|
|
@ -65,7 +65,7 @@ class User extends DataClass implements Insertable<User> {
|
|||
@override
|
||||
Map<String, dynamic> toJson(
|
||||
{ValueSerializer serializer = const ValueSerializer.defaults()}) {
|
||||
return {
|
||||
return <String, dynamic>{
|
||||
'id': serializer.toJson<int>(id),
|
||||
'name': serializer.toJson<String>(name),
|
||||
'born_on': serializer.toJson<DateTime>(birthDate),
|
||||
|
@ -124,7 +124,7 @@ class User extends DataClass implements Insertable<User> {
|
|||
$mrjc(birthDate.hashCode,
|
||||
$mrjc(profilePicture.hashCode, preferences.hashCode)))));
|
||||
@override
|
||||
bool operator ==(other) =>
|
||||
bool operator ==(dynamic other) =>
|
||||
identical(this, other) ||
|
||||
(other is User &&
|
||||
other.id == this.id &&
|
||||
|
@ -351,7 +351,7 @@ class Friendship extends DataClass implements Insertable<Friendship> {
|
|||
@override
|
||||
Map<String, dynamic> toJson(
|
||||
{ValueSerializer serializer = const ValueSerializer.defaults()}) {
|
||||
return {
|
||||
return <String, dynamic>{
|
||||
'firstUser': serializer.toJson<int>(firstUser),
|
||||
'secondUser': serializer.toJson<int>(secondUser),
|
||||
'reallyGoodFriends': serializer.toJson<bool>(reallyGoodFriends),
|
||||
|
@ -394,7 +394,7 @@ class Friendship extends DataClass implements Insertable<Friendship> {
|
|||
int get hashCode => $mrjf($mrjc(firstUser.hashCode,
|
||||
$mrjc(secondUser.hashCode, reallyGoodFriends.hashCode)));
|
||||
@override
|
||||
bool operator ==(other) =>
|
||||
bool operator ==(dynamic other) =>
|
||||
identical(this, other) ||
|
||||
(other is Friendship &&
|
||||
other.firstUser == this.firstUser &&
|
||||
|
|
|
@ -13,9 +13,9 @@ void crudTests(TestExecutor executor) {
|
|||
final expectation = expectLater(
|
||||
friends,
|
||||
emitsInOrder(
|
||||
[
|
||||
<Matcher>[
|
||||
isEmpty, // initial state without friendships
|
||||
[b] // after we called makeFriends(a,b)
|
||||
equals(<User>[b]), // after we called makeFriends(a,b)
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:test/test.dart';
|
||||
import 'package:tests/data/sample_data.dart';
|
||||
import 'package:tests/data/sample_data.dart' as people;
|
||||
import 'package:tests/database/database.dart';
|
||||
|
||||
import 'suite.dart';
|
||||
|
@ -17,7 +17,7 @@ void migrationTests(TestExecutor executor) {
|
|||
|
||||
test('saves and restores database', () async {
|
||||
var database = Database(executor.createExecutor(), schemaVersion: 1);
|
||||
await database.writeUser(People.florian);
|
||||
await database.writeUser(people.florian);
|
||||
await database.close();
|
||||
|
||||
database = Database(executor.createExecutor(), schemaVersion: 2);
|
||||
|
|
|
@ -14,6 +14,8 @@ abstract class TestExecutor {
|
|||
}
|
||||
|
||||
void runAllTests(TestExecutor executor) {
|
||||
moorRuntimeOptions.dontWarnAboutMultipleDatabases = true;
|
||||
|
||||
tearDown(() async {
|
||||
await executor.deleteData();
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:test/test.dart';
|
||||
import 'package:tests/data/sample_data.dart';
|
||||
import 'package:tests/data/sample_data.dart' as people;
|
||||
import 'package:tests/database/database.dart';
|
||||
|
||||
import 'suite.dart';
|
||||
|
@ -10,9 +10,9 @@ void transactionTests(TestExecutor executor) {
|
|||
|
||||
// ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
|
||||
await db.transaction(() async {
|
||||
final florianId = await db.writeUser(People.florian);
|
||||
final florianId = await db.writeUser(people.florian);
|
||||
|
||||
final dash = await db.getUserById(People.dashId);
|
||||
final dash = await db.getUserById(people.dashId);
|
||||
final florian = await db.getUserById(florianId);
|
||||
|
||||
await db.makeFriends(dash, florian, goodFriends: true);
|
||||
|
@ -21,7 +21,7 @@ void transactionTests(TestExecutor executor) {
|
|||
final countResult = await db.userCount();
|
||||
expect(countResult.single, 4);
|
||||
|
||||
final friendsResult = await db.amountOfGoodFriends(People.dashId);
|
||||
final friendsResult = await db.amountOfGoodFriends(people.dashId);
|
||||
expect(friendsResult.single, 1);
|
||||
|
||||
await db.close();
|
||||
|
@ -33,9 +33,9 @@ void transactionTests(TestExecutor executor) {
|
|||
try {
|
||||
// ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
|
||||
await db.transaction(() async {
|
||||
final florianId = await db.writeUser(People.florian);
|
||||
final florianId = await db.writeUser(people.florian);
|
||||
|
||||
final dash = await db.getUserById(People.dashId);
|
||||
final dash = await db.getUserById(people.dashId);
|
||||
final florian = await db.getUserById(florianId);
|
||||
|
||||
await db.makeFriends(dash, florian, goodFriends: true);
|
||||
|
@ -47,7 +47,7 @@ void transactionTests(TestExecutor executor) {
|
|||
final countResult = await db.userCount();
|
||||
expect(countResult.single, 3); // only the default folks
|
||||
|
||||
final friendsResult = await db.amountOfGoodFriends(People.dashId);
|
||||
final friendsResult = await db.amountOfGoodFriends(people.dashId);
|
||||
expect(friendsResult.single, 0); // no friendship was inserted
|
||||
|
||||
await db.close();
|
||||
|
|
|
@ -11,11 +11,11 @@ dependencies:
|
|||
moor:
|
||||
path:
|
||||
../../../moor
|
||||
json_annotation:
|
||||
json_annotation: ^3.0.0
|
||||
dev_dependencies:
|
||||
build_runner:
|
||||
moor_generator:
|
||||
json_serializable:
|
||||
build_runner: ^1.7.2
|
||||
moor_generator: ^2.0.0
|
||||
json_serializable: ^3.2.3
|
||||
test:
|
||||
|
||||
dependency_overrides:
|
||||
|
|
|
@ -7,7 +7,7 @@ import 'package:path/path.dart' show join;
|
|||
|
||||
class VmExecutor extends TestExecutor {
|
||||
static String fileName = 'moor-vm-tests-${DateTime.now().toIso8601String()}';
|
||||
final file = File(join(Directory.systemTemp.path, fileName));
|
||||
final File file = File(join(Directory.systemTemp.path, fileName));
|
||||
|
||||
@override
|
||||
QueryExecutor createExecutor() {
|
||||
|
|
|
@ -6,7 +6,7 @@ import 'package:test/test.dart';
|
|||
import 'package:moor/moor_web.dart';
|
||||
|
||||
class WebExecutor extends TestExecutor {
|
||||
final name = 'db';
|
||||
final String name = 'db';
|
||||
|
||||
@override
|
||||
QueryExecutor createExecutor() {
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
@TestOn('browser')
|
||||
import 'dart:html';
|
||||
|
||||
import 'package:moor/moor_web.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
part 'saves_after_migration_regression_test.g.dart';
|
||||
|
||||
// This is a regression test for https://github.com/simolus3/moor/issues/273
|
||||
|
||||
class Foos extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
}
|
||||
|
||||
class Bars extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
}
|
||||
|
||||
@UseMoor(
|
||||
tables: [Foos, Bars],
|
||||
)
|
||||
class _FakeDb extends _$_FakeDb {
|
||||
@override
|
||||
final int schemaVersion;
|
||||
|
||||
_FakeDb(QueryExecutor executor, this.schemaVersion) : super(executor);
|
||||
|
||||
@override
|
||||
List<TableInfo<Table, DataClass>> get allTables => [
|
||||
foos,
|
||||
if (schemaVersion >= 2) bars,
|
||||
];
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration =>
|
||||
MigrationStrategy(onUpgrade: (m, from, to) async {
|
||||
await m.createTable(bars);
|
||||
});
|
||||
}
|
||||
|
||||
void main() {
|
||||
tearDown(() {
|
||||
window.localStorage.clear();
|
||||
});
|
||||
|
||||
test('saves the database after creating it', () async {
|
||||
var db = _FakeDb(WebDatabase('foo'), 1);
|
||||
// ensure the database is opened
|
||||
await db.customSelectQuery('SELECT 1').get();
|
||||
|
||||
await db.close();
|
||||
db = _FakeDb(WebDatabase('foo'), 1);
|
||||
|
||||
await db.select(db.foos).get(); // shouldn't throw, table exists
|
||||
await db.close();
|
||||
});
|
||||
|
||||
test('saves the database after an update', () async {
|
||||
var db = _FakeDb(WebDatabase('foo'), 1);
|
||||
await db.customSelectQuery('SELECT 1').get();
|
||||
await db.close();
|
||||
|
||||
// run a migration to version 2
|
||||
db = _FakeDb(WebDatabase('foo'), 2);
|
||||
await db.customSelectQuery('SELECT 1').get();
|
||||
await db.close();
|
||||
|
||||
db = _FakeDb(WebDatabase('foo'), 2);
|
||||
await db.select(db.bars).get(); // shouldn't throw, table exists
|
||||
await db.close();
|
||||
});
|
||||
}
|
|
@ -0,0 +1,263 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'saves_after_migration_regression_test.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// MoorGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// ignore_for_file: unnecessary_brace_in_string_interps, unnecessary_this
|
||||
class Foo extends DataClass implements Insertable<Foo> {
|
||||
final int id;
|
||||
Foo({@required this.id});
|
||||
factory Foo.fromData(Map<String, dynamic> data, GeneratedDatabase db,
|
||||
{String prefix}) {
|
||||
final effectivePrefix = prefix ?? '';
|
||||
final intType = db.typeSystem.forDartType<int>();
|
||||
return Foo(
|
||||
id: intType.mapFromDatabaseResponse(data['${effectivePrefix}id']),
|
||||
);
|
||||
}
|
||||
factory Foo.fromJson(Map<String, dynamic> json,
|
||||
{ValueSerializer serializer = const ValueSerializer.defaults()}) {
|
||||
return Foo(
|
||||
id: serializer.fromJson<int>(json['id']),
|
||||
);
|
||||
}
|
||||
factory Foo.fromJsonString(String encodedJson,
|
||||
{ValueSerializer serializer = const ValueSerializer.defaults()}) =>
|
||||
Foo.fromJson(DataClass.parseJson(encodedJson) as Map<String, dynamic>,
|
||||
serializer: serializer);
|
||||
@override
|
||||
Map<String, dynamic> toJson(
|
||||
{ValueSerializer serializer = const ValueSerializer.defaults()}) {
|
||||
return <String, dynamic>{
|
||||
'id': serializer.toJson<int>(id),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
FoosCompanion createCompanion(bool nullToAbsent) {
|
||||
return FoosCompanion(
|
||||
id: id == null && nullToAbsent ? const Value.absent() : Value(id),
|
||||
);
|
||||
}
|
||||
|
||||
Foo copyWith({int id}) => Foo(
|
||||
id: id ?? this.id,
|
||||
);
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('Foo(')..write('id: $id')..write(')')).toString();
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => $mrjf(id.hashCode);
|
||||
@override
|
||||
bool operator ==(dynamic other) =>
|
||||
identical(this, other) || (other is Foo && other.id == this.id);
|
||||
}
|
||||
|
||||
class FoosCompanion extends UpdateCompanion<Foo> {
|
||||
final Value<int> id;
|
||||
const FoosCompanion({
|
||||
this.id = const Value.absent(),
|
||||
});
|
||||
FoosCompanion.insert({
|
||||
this.id = const Value.absent(),
|
||||
});
|
||||
FoosCompanion copyWith({Value<int> id}) {
|
||||
return FoosCompanion(
|
||||
id: id ?? this.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class $FoosTable extends Foos with TableInfo<$FoosTable, Foo> {
|
||||
final GeneratedDatabase _db;
|
||||
final String _alias;
|
||||
$FoosTable(this._db, [this._alias]);
|
||||
final VerificationMeta _idMeta = const VerificationMeta('id');
|
||||
GeneratedIntColumn _id;
|
||||
@override
|
||||
GeneratedIntColumn get id => _id ??= _constructId();
|
||||
GeneratedIntColumn _constructId() {
|
||||
return GeneratedIntColumn('id', $tableName, false,
|
||||
hasAutoIncrement: true, declaredAsPrimaryKey: true);
|
||||
}
|
||||
|
||||
@override
|
||||
List<GeneratedColumn> get $columns => [id];
|
||||
@override
|
||||
$FoosTable get asDslTable => this;
|
||||
@override
|
||||
String get $tableName => _alias ?? 'foos';
|
||||
@override
|
||||
final String actualTableName = 'foos';
|
||||
@override
|
||||
VerificationContext validateIntegrity(FoosCompanion d,
|
||||
{bool isInserting = false}) {
|
||||
final context = VerificationContext();
|
||||
if (d.id.present) {
|
||||
context.handle(_idMeta, id.isAcceptableValue(d.id.value, _idMeta));
|
||||
} else if (id.isRequired && isInserting) {
|
||||
context.missing(_idMeta);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {id};
|
||||
@override
|
||||
Foo map(Map<String, dynamic> data, {String tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : null;
|
||||
return Foo.fromData(data, _db, prefix: effectivePrefix);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Variable> entityToSql(FoosCompanion d) {
|
||||
final map = <String, Variable>{};
|
||||
if (d.id.present) {
|
||||
map['id'] = Variable<int, IntType>(d.id.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
$FoosTable createAlias(String alias) {
|
||||
return $FoosTable(_db, alias);
|
||||
}
|
||||
}
|
||||
|
||||
class Bar extends DataClass implements Insertable<Bar> {
|
||||
final int id;
|
||||
Bar({@required this.id});
|
||||
factory Bar.fromData(Map<String, dynamic> data, GeneratedDatabase db,
|
||||
{String prefix}) {
|
||||
final effectivePrefix = prefix ?? '';
|
||||
final intType = db.typeSystem.forDartType<int>();
|
||||
return Bar(
|
||||
id: intType.mapFromDatabaseResponse(data['${effectivePrefix}id']),
|
||||
);
|
||||
}
|
||||
factory Bar.fromJson(Map<String, dynamic> json,
|
||||
{ValueSerializer serializer = const ValueSerializer.defaults()}) {
|
||||
return Bar(
|
||||
id: serializer.fromJson<int>(json['id']),
|
||||
);
|
||||
}
|
||||
factory Bar.fromJsonString(String encodedJson,
|
||||
{ValueSerializer serializer = const ValueSerializer.defaults()}) =>
|
||||
Bar.fromJson(DataClass.parseJson(encodedJson) as Map<String, dynamic>,
|
||||
serializer: serializer);
|
||||
@override
|
||||
Map<String, dynamic> toJson(
|
||||
{ValueSerializer serializer = const ValueSerializer.defaults()}) {
|
||||
return <String, dynamic>{
|
||||
'id': serializer.toJson<int>(id),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
BarsCompanion createCompanion(bool nullToAbsent) {
|
||||
return BarsCompanion(
|
||||
id: id == null && nullToAbsent ? const Value.absent() : Value(id),
|
||||
);
|
||||
}
|
||||
|
||||
Bar copyWith({int id}) => Bar(
|
||||
id: id ?? this.id,
|
||||
);
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('Bar(')..write('id: $id')..write(')')).toString();
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => $mrjf(id.hashCode);
|
||||
@override
|
||||
bool operator ==(dynamic other) =>
|
||||
identical(this, other) || (other is Bar && other.id == this.id);
|
||||
}
|
||||
|
||||
class BarsCompanion extends UpdateCompanion<Bar> {
|
||||
final Value<int> id;
|
||||
const BarsCompanion({
|
||||
this.id = const Value.absent(),
|
||||
});
|
||||
BarsCompanion.insert({
|
||||
this.id = const Value.absent(),
|
||||
});
|
||||
BarsCompanion copyWith({Value<int> id}) {
|
||||
return BarsCompanion(
|
||||
id: id ?? this.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class $BarsTable extends Bars with TableInfo<$BarsTable, Bar> {
|
||||
final GeneratedDatabase _db;
|
||||
final String _alias;
|
||||
$BarsTable(this._db, [this._alias]);
|
||||
final VerificationMeta _idMeta = const VerificationMeta('id');
|
||||
GeneratedIntColumn _id;
|
||||
@override
|
||||
GeneratedIntColumn get id => _id ??= _constructId();
|
||||
GeneratedIntColumn _constructId() {
|
||||
return GeneratedIntColumn('id', $tableName, false,
|
||||
hasAutoIncrement: true, declaredAsPrimaryKey: true);
|
||||
}
|
||||
|
||||
@override
|
||||
List<GeneratedColumn> get $columns => [id];
|
||||
@override
|
||||
$BarsTable get asDslTable => this;
|
||||
@override
|
||||
String get $tableName => _alias ?? 'bars';
|
||||
@override
|
||||
final String actualTableName = 'bars';
|
||||
@override
|
||||
VerificationContext validateIntegrity(BarsCompanion d,
|
||||
{bool isInserting = false}) {
|
||||
final context = VerificationContext();
|
||||
if (d.id.present) {
|
||||
context.handle(_idMeta, id.isAcceptableValue(d.id.value, _idMeta));
|
||||
} else if (id.isRequired && isInserting) {
|
||||
context.missing(_idMeta);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {id};
|
||||
@override
|
||||
Bar map(Map<String, dynamic> data, {String tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : null;
|
||||
return Bar.fromData(data, _db, prefix: effectivePrefix);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Variable> entityToSql(BarsCompanion d) {
|
||||
final map = <String, Variable>{};
|
||||
if (d.id.present) {
|
||||
map['id'] = Variable<int, IntType>(d.id.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
$BarsTable createAlias(String alias) {
|
||||
return $BarsTable(_db, alias);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _$_FakeDb extends GeneratedDatabase {
|
||||
_$_FakeDb(QueryExecutor e) : super(SqlTypeSystem.defaultInstance, e);
|
||||
_$_FakeDb.connect(DatabaseConnection c) : super.connect(c);
|
||||
$FoosTable _foos;
|
||||
$FoosTable get foos => _foos ??= $FoosTable(this);
|
||||
$BarsTable _bars;
|
||||
$BarsTable get bars => _bars ??= $BarsTable(this);
|
||||
@override
|
||||
List<TableInfo> get allTables => [foos, bars];
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title></title>
|
||||
<link rel="x-dart-test" href="saves_after_migration_regression_test.dart">
|
||||
<script src="sql-wasm.js"></script>
|
||||
<script src="packages/test/dart.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
|
@ -18,7 +18,7 @@ mixin _MySqlExecutor on QueryDelegate {
|
|||
|
||||
@override
|
||||
Future<void> runBatched(List<BatchedStatement> statements) async {
|
||||
for (var stmt in statements) {
|
||||
for (final stmt in statements) {
|
||||
await _querier.preparedMulti(stmt.sql, stmt.variables);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ environment:
|
|||
sdk: '>=2.4.0 <3.0.0'
|
||||
|
||||
dependencies:
|
||||
moor: ^1.6.0
|
||||
moor: ^2.0.0
|
||||
sqljocky5: ^2.2.0
|
||||
|
||||
dev_dependencies:
|
||||
|
|
|
@ -10,7 +10,7 @@ Currently, we support
|
|||
- navigation for references in sql queries
|
||||
|
||||
## Setup
|
||||
To debug this plugin, you'll need to perform these steps once. It is assumed that you
|
||||
To use this plugin, you'll need to perform these steps once. It is assumed that you
|
||||
have already cloned the `moor` repository.
|
||||
|
||||
1. Make sure you run version `3.5.0` or later of the Dart extension in VS Code.
|
||||
|
@ -23,16 +23,20 @@ have already cloned the `moor` repository.
|
|||
```
|
||||
3. Uncomment the plugin lines in `analysis_options.yaml`
|
||||
|
||||
## Running
|
||||
## Debugging
|
||||
Note: If you only want to _use_ the plugin and don't care about debugging it, follow the step
|
||||
from the [user documentation](https://moor.simonbinder.eu/docs/using-sql/sql_ide/).
|
||||
|
||||
After you completed the setup, these steps will open an editor instance that runs the plugin.
|
||||
1. chdir into `moor_generator` and run `lib/plugin.dart`. You can run that file from an IDE if
|
||||
you need debugging capabilities, but starting it from the command line is fine. Keep that
|
||||
script running.
|
||||
2. Open this folder in the code instance
|
||||
3. Wait ~15s, you should start to see some log entries in the output of step 1.
|
||||
1. chdir into `moor_generator` and run `dart bin/moor_generator.dart debug-plugin`.
|
||||
You can run that script from an IDE if you need debugging capabilities, but starting
|
||||
it from the command line is fine. Keep that script running.
|
||||
3. Uncomment the `plugin` lines in `analysis_options.yaml`
|
||||
3. Open this folder in the code instance
|
||||
4. Wait ~15s, you should start to see some log entries in the output of step 1.
|
||||
As soon as they appear, the plugin is ready to go.
|
||||
|
||||
_Note_: `lib/plugin.dart` doesn't support multiple clients. Whenever you close or reload the
|
||||
_Note_: `bin/moor_generator.dart` doesn't support multiple clients. Whenever you close or reload the
|
||||
editor, that script needs to be restarted as well. That script should also be running before
|
||||
starting the analysis server.
|
||||
|
||||
|
@ -40,7 +44,7 @@ starting the analysis server.
|
|||
|
||||
If the plugin doesn't start properly, you can
|
||||
|
||||
1. make sure it was picked up by the analysis server: You set the `dart.analyzerDiagnosticsPort`
|
||||
1. make sure it was picked up by the analysis server: Set the `dart.analyzerDiagnosticsPort`
|
||||
to any port and see some basic information under the "plugins" tab of the website started.
|
||||
2. When setting `dart.analyzerInstrumentationLogFile`, the analysis server will write the
|
||||
exception that caused the plugin to stop
|
|
@ -32,8 +32,7 @@ android/
|
|||
ios/
|
||||
|
||||
# coverage
|
||||
test/.test_coverage.dart
|
||||
coverage_badge.svg
|
||||
coverage/
|
||||
|
||||
### Intellij ###
|
||||
.idea/**/*
|
||||
|
|
|
@ -1,6 +1,57 @@
|
|||
## unreleased
|
||||
|
||||
- New `clientDefault` method for columns. It can be used for dynamic defaults that might be different for
|
||||
each row. For instance, you can generate a uuid for each row with `text().clientDefault(() => Uuid().v4()();`
|
||||
- Ability to override the default `ValueSerializer` globally by using `moorRuntimeOptions.valueSerializer`.
|
||||
- Moor files: You can now explicitly declare column types in those cases that the analyzer can't
|
||||
infer it:
|
||||
```
|
||||
selectVariable(:variable AS TEXT): SELECT :variable;
|
||||
```
|
||||
|
||||
## 2.2.0
|
||||
|
||||
- Support custom expressions for selects in the Dart API:
|
||||
```dart
|
||||
final currentBalance = accounts.income - accounts.expenses;
|
||||
select(accounts).addColumns([currentBalance]).map((row) {
|
||||
Account account = row.readTable(accounts);
|
||||
int balanceOfAccount = row.read(currentBalance);
|
||||
return ...
|
||||
}).get();
|
||||
```
|
||||
- Support the `json1` and `fts5` extensions! Using them also requires version 2.2 of `moor_generator`
|
||||
and they require `moor_ffi`. For details, see the [documentation](https://moor.simonbinder.eu/docs/using-sql/extensions/).
|
||||
- Standardized behavior of batches in transactions across backends
|
||||
- Introduced `OrderingTerm.asc` and `OrderingTerm.desc` factories to construct ordering terms more
|
||||
easily
|
||||
|
||||
## 2.1.1
|
||||
|
||||
- Fix crash when closing a database with asserts disabled
|
||||
- Web: Save the database after migrations ran
|
||||
- Escape column names in insert statements, if necessary
|
||||
|
||||
## 2.1.0
|
||||
|
||||
- New extension methods to simplify the Dart api!
|
||||
- Use `&`, `or` and `.not()` to combine boolean expressions.
|
||||
```dart
|
||||
// OLD
|
||||
select(animals)..where((a) => and(not(a.isMammal), a.amountOfLegs.equals(4)))
|
||||
// NEW:
|
||||
select(animals)..where((a) => a.isMammal.not() & a.amountOfLegs.equals(4))
|
||||
```
|
||||
- Arithmetic: New `+`, `-`, `*` and `/` operators for int and double sql expressions
|
||||
- New `+` operator for string concatenation
|
||||
- Fix crash when `customStatement` is the first operation used on a database ([#199](https://github.com/simolus3/moor/issues/199))
|
||||
- Allow transactions inside a `beforeOpen` callback
|
||||
- New `batch` method on generated databases to execute multiple queries in a single batch
|
||||
- Experimental support to run moor on a background isolate
|
||||
- Reduce use of parentheses in SQL code generated at runtime
|
||||
- Query streams now emit errors that happened while running the query
|
||||
- Upgraded the sql parser which now supports `WITH` clauses in moor files
|
||||
- Internal refactorings on the runtime query builder
|
||||
|
||||
## 2.0.1
|
||||
|
||||
|
@ -11,6 +62,9 @@
|
|||
- Fix streams not emitting cached data when listening multiple times
|
||||
- __Breaking__: Remove the type parameter from `Insertable.createCompanion` (it was declared as an
|
||||
internal method)
|
||||
|
||||
__2.0.1+1__: Fix crash when `customStatement` is the first operation used on a database
|
||||
([#199](https://github.com/simolus3/moor/issues/199))
|
||||
|
||||
## 2.0.0
|
||||
This is the first major update after the initial release and moor and we have a lot to cover:
|
||||
|
|
|
@ -1,18 +1,32 @@
|
|||
# moor
|
||||
# Moor
|
||||
Moor is a reactive persistence library for Flutter and Dart, built ontop of
|
||||
sqlite.
|
||||
Moor is
|
||||
|
||||
Moor is an easy to use, reactive persistence library for Flutter apps.
|
||||
Define your database tables in pure Dart and enjoy a fluent query API,
|
||||
auto-updating streams and more!
|
||||
- __Flexible__: Moor let's you write queries in both SQL and Dart,
|
||||
providing fluent apis for both languages. You can filter and order results
|
||||
or use joins to run queries on multiple tables. You can even use complex
|
||||
sql features like `WITH` and `WINDOW` clauses.
|
||||
- __🔥 Feature rich__: Moor has builtin support for transactions, schema
|
||||
migrations, complex filters and expressions, batched updates and joins. We
|
||||
even have a builtin IDE for SQL!
|
||||
- __📦 Modular__: Thanks to builtin support for daos and `import`s in sql files, moor helps you keep your database code simple.
|
||||
- __🛡️ Safe__: Moor generates typesafe code based on your tables and queries. If you make a mistake in your queries, moor will find it at compile time and
|
||||
provide helpful and descriptive lints.
|
||||
- __⚡ Fast__: Even though moor lets you write powerful queries, it can keep
|
||||
up with the performance of key-value stores. Moor is the only major persistence library with builtin threading support, allowing you to run database code across isolates with zero additional effort.
|
||||
- __Reactive__: Turn any sql query into an auto-updating stream! This includes complex queries across many tables
|
||||
- __⚙️ Cross-Platform support__: Moor works on Android, iOS, macOS, Windows, Linux and the web. [This template](https://github.com/appleeducate/moor_shared) is a Flutter todo app that works on all platforms
|
||||
- __🗡️ Battle tested and production ready__: Moor is stable and well tested with a wide range of unit and integration tests. It powers production Flutter apps.
|
||||
|
||||
## With Flutter
|
||||
This library defines the APIs for the moor persistence library. To use it on Flutter apps,
|
||||
you'll probably want to use the moor_flutter implementation directly.
|
||||
With moor, persistence on Flutter is fun!
|
||||
|
||||
__To start using moor, read our detailed [docs](https://moor.simonbinder.eu/docs/getting-started/).__
|
||||
|
||||
If you have any questions, feedback or ideas, feel free to [create an
|
||||
issue](https://github.com/simolus3/moor/issues/new). If you enjoy this
|
||||
project, I'd appreciate your [🌟 on GitHub](https://github.com/simolus3/moor/).
|
||||
|
||||
## For the web
|
||||
For information to use this library on the web (including Flutter web), follow the
|
||||
instructions [here](https://moor.simonbinder.eu/web). Keep in mind that web support is
|
||||
still experimental.
|
||||
|
||||
Please see the homepage of [moor](https://moor.simonbinder.eu/) or
|
||||
the [moor_flutter package](https://pub.dartlang.org/packages/moor_flutter) for details
|
||||
on how to use this package.
|
||||
instructions [here](https://moor.simonbinder.eu/web). Keep in mind that web support is still experimental.
|
|
@ -4,4 +4,9 @@ targets:
|
|||
moor_generator:
|
||||
options:
|
||||
override_hash_and_equals_in_result_sets: true
|
||||
use_column_name_as_json_key_when_defined_in_moor_file: true
|
||||
use_column_name_as_json_key_when_defined_in_moor_file: true
|
||||
generate_connect_constructor: true
|
||||
write_from_json_string_constructor: true
|
||||
sqlite_modules:
|
||||
- json1
|
||||
- fts5
|
|
@ -0,0 +1,2 @@
|
|||
tags:
|
||||
integration:
|
|
@ -23,16 +23,22 @@ class Category extends DataClass implements Insertable<Category> {
|
|||
);
|
||||
}
|
||||
factory Category.fromJson(Map<String, dynamic> json,
|
||||
{ValueSerializer serializer = const ValueSerializer.defaults()}) {
|
||||
{ValueSerializer serializer}) {
|
||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||
return Category(
|
||||
id: serializer.fromJson<int>(json['id']),
|
||||
description: serializer.fromJson<String>(json['description']),
|
||||
);
|
||||
}
|
||||
factory Category.fromJsonString(String encodedJson,
|
||||
{ValueSerializer serializer}) =>
|
||||
Category.fromJson(
|
||||
DataClass.parseJson(encodedJson) as Map<String, dynamic>,
|
||||
serializer: serializer);
|
||||
@override
|
||||
Map<String, dynamic> toJson(
|
||||
{ValueSerializer serializer = const ValueSerializer.defaults()}) {
|
||||
return {
|
||||
Map<String, dynamic> toJson({ValueSerializer serializer}) {
|
||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'id': serializer.toJson<int>(id),
|
||||
'description': serializer.toJson<String>(description),
|
||||
};
|
||||
|
@ -64,7 +70,7 @@ class Category extends DataClass implements Insertable<Category> {
|
|||
@override
|
||||
int get hashCode => $mrjf($mrjc(id.hashCode, description.hashCode));
|
||||
@override
|
||||
bool operator ==(other) =>
|
||||
bool operator ==(dynamic other) =>
|
||||
identical(this, other) ||
|
||||
(other is Category &&
|
||||
other.id == this.id &&
|
||||
|
@ -132,14 +138,10 @@ class $CategoriesTable extends Categories
|
|||
final context = VerificationContext();
|
||||
if (d.id.present) {
|
||||
context.handle(_idMeta, id.isAcceptableValue(d.id.value, _idMeta));
|
||||
} else if (id.isRequired && isInserting) {
|
||||
context.missing(_idMeta);
|
||||
}
|
||||
if (d.description.present) {
|
||||
context.handle(_descriptionMeta,
|
||||
description.isAcceptableValue(d.description.value, _descriptionMeta));
|
||||
} else if (description.isRequired && isInserting) {
|
||||
context.missing(_descriptionMeta);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
@ -196,7 +198,8 @@ class Recipe extends DataClass implements Insertable<Recipe> {
|
|||
);
|
||||
}
|
||||
factory Recipe.fromJson(Map<String, dynamic> json,
|
||||
{ValueSerializer serializer = const ValueSerializer.defaults()}) {
|
||||
{ValueSerializer serializer}) {
|
||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||
return Recipe(
|
||||
id: serializer.fromJson<int>(json['id']),
|
||||
title: serializer.fromJson<String>(json['title']),
|
||||
|
@ -204,10 +207,14 @@ class Recipe extends DataClass implements Insertable<Recipe> {
|
|||
category: serializer.fromJson<int>(json['category']),
|
||||
);
|
||||
}
|
||||
factory Recipe.fromJsonString(String encodedJson,
|
||||
{ValueSerializer serializer}) =>
|
||||
Recipe.fromJson(DataClass.parseJson(encodedJson) as Map<String, dynamic>,
|
||||
serializer: serializer);
|
||||
@override
|
||||
Map<String, dynamic> toJson(
|
||||
{ValueSerializer serializer = const ValueSerializer.defaults()}) {
|
||||
return {
|
||||
Map<String, dynamic> toJson({ValueSerializer serializer}) {
|
||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'id': serializer.toJson<int>(id),
|
||||
'title': serializer.toJson<String>(title),
|
||||
'instructions': serializer.toJson<String>(instructions),
|
||||
|
@ -252,7 +259,7 @@ class Recipe extends DataClass implements Insertable<Recipe> {
|
|||
int get hashCode => $mrjf($mrjc(id.hashCode,
|
||||
$mrjc(title.hashCode, $mrjc(instructions.hashCode, category.hashCode))));
|
||||
@override
|
||||
bool operator ==(other) =>
|
||||
bool operator ==(dynamic other) =>
|
||||
identical(this, other) ||
|
||||
(other is Recipe &&
|
||||
other.id == this.id &&
|
||||
|
@ -354,13 +361,11 @@ class $RecipesTable extends Recipes with TableInfo<$RecipesTable, Recipe> {
|
|||
final context = VerificationContext();
|
||||
if (d.id.present) {
|
||||
context.handle(_idMeta, id.isAcceptableValue(d.id.value, _idMeta));
|
||||
} else if (id.isRequired && isInserting) {
|
||||
context.missing(_idMeta);
|
||||
}
|
||||
if (d.title.present) {
|
||||
context.handle(
|
||||
_titleMeta, title.isAcceptableValue(d.title.value, _titleMeta));
|
||||
} else if (title.isRequired && isInserting) {
|
||||
} else if (isInserting) {
|
||||
context.missing(_titleMeta);
|
||||
}
|
||||
if (d.instructions.present) {
|
||||
|
@ -368,14 +373,12 @@ class $RecipesTable extends Recipes with TableInfo<$RecipesTable, Recipe> {
|
|||
_instructionsMeta,
|
||||
instructions.isAcceptableValue(
|
||||
d.instructions.value, _instructionsMeta));
|
||||
} else if (instructions.isRequired && isInserting) {
|
||||
} else if (isInserting) {
|
||||
context.missing(_instructionsMeta);
|
||||
}
|
||||
if (d.category.present) {
|
||||
context.handle(_categoryMeta,
|
||||
category.isAcceptableValue(d.category.value, _categoryMeta));
|
||||
} else if (category.isRequired && isInserting) {
|
||||
context.missing(_categoryMeta);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
@ -431,17 +434,23 @@ class Ingredient extends DataClass implements Insertable<Ingredient> {
|
|||
);
|
||||
}
|
||||
factory Ingredient.fromJson(Map<String, dynamic> json,
|
||||
{ValueSerializer serializer = const ValueSerializer.defaults()}) {
|
||||
{ValueSerializer serializer}) {
|
||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||
return Ingredient(
|
||||
id: serializer.fromJson<int>(json['id']),
|
||||
name: serializer.fromJson<String>(json['name']),
|
||||
caloriesPer100g: serializer.fromJson<int>(json['caloriesPer100g']),
|
||||
);
|
||||
}
|
||||
factory Ingredient.fromJsonString(String encodedJson,
|
||||
{ValueSerializer serializer}) =>
|
||||
Ingredient.fromJson(
|
||||
DataClass.parseJson(encodedJson) as Map<String, dynamic>,
|
||||
serializer: serializer);
|
||||
@override
|
||||
Map<String, dynamic> toJson(
|
||||
{ValueSerializer serializer = const ValueSerializer.defaults()}) {
|
||||
return {
|
||||
Map<String, dynamic> toJson({ValueSerializer serializer}) {
|
||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'id': serializer.toJson<int>(id),
|
||||
'name': serializer.toJson<String>(name),
|
||||
'caloriesPer100g': serializer.toJson<int>(caloriesPer100g),
|
||||
|
@ -478,7 +487,7 @@ class Ingredient extends DataClass implements Insertable<Ingredient> {
|
|||
int get hashCode =>
|
||||
$mrjf($mrjc(id.hashCode, $mrjc(name.hashCode, caloriesPer100g.hashCode)));
|
||||
@override
|
||||
bool operator ==(other) =>
|
||||
bool operator ==(dynamic other) =>
|
||||
identical(this, other) ||
|
||||
(other is Ingredient &&
|
||||
other.id == this.id &&
|
||||
|
@ -565,13 +574,11 @@ class $IngredientsTable extends Ingredients
|
|||
final context = VerificationContext();
|
||||
if (d.id.present) {
|
||||
context.handle(_idMeta, id.isAcceptableValue(d.id.value, _idMeta));
|
||||
} else if (id.isRequired && isInserting) {
|
||||
context.missing(_idMeta);
|
||||
}
|
||||
if (d.name.present) {
|
||||
context.handle(
|
||||
_nameMeta, name.isAcceptableValue(d.name.value, _nameMeta));
|
||||
} else if (name.isRequired && isInserting) {
|
||||
} else if (isInserting) {
|
||||
context.missing(_nameMeta);
|
||||
}
|
||||
if (d.caloriesPer100g.present) {
|
||||
|
@ -579,7 +586,7 @@ class $IngredientsTable extends Ingredients
|
|||
_caloriesPer100gMeta,
|
||||
caloriesPer100g.isAcceptableValue(
|
||||
d.caloriesPer100g.value, _caloriesPer100gMeta));
|
||||
} else if (caloriesPer100g.isRequired && isInserting) {
|
||||
} else if (isInserting) {
|
||||
context.missing(_caloriesPer100gMeta);
|
||||
}
|
||||
return context;
|
||||
|
@ -637,17 +644,23 @@ class IngredientInRecipe extends DataClass
|
|||
);
|
||||
}
|
||||
factory IngredientInRecipe.fromJson(Map<String, dynamic> json,
|
||||
{ValueSerializer serializer = const ValueSerializer.defaults()}) {
|
||||
{ValueSerializer serializer}) {
|
||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||
return IngredientInRecipe(
|
||||
recipe: serializer.fromJson<int>(json['recipe']),
|
||||
ingredient: serializer.fromJson<int>(json['ingredient']),
|
||||
amountInGrams: serializer.fromJson<int>(json['amountInGrams']),
|
||||
);
|
||||
}
|
||||
factory IngredientInRecipe.fromJsonString(String encodedJson,
|
||||
{ValueSerializer serializer}) =>
|
||||
IngredientInRecipe.fromJson(
|
||||
DataClass.parseJson(encodedJson) as Map<String, dynamic>,
|
||||
serializer: serializer);
|
||||
@override
|
||||
Map<String, dynamic> toJson(
|
||||
{ValueSerializer serializer = const ValueSerializer.defaults()}) {
|
||||
return {
|
||||
Map<String, dynamic> toJson({ValueSerializer serializer}) {
|
||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'recipe': serializer.toJson<int>(recipe),
|
||||
'ingredient': serializer.toJson<int>(ingredient),
|
||||
'amountInGrams': serializer.toJson<int>(amountInGrams),
|
||||
|
@ -689,7 +702,7 @@ class IngredientInRecipe extends DataClass
|
|||
int get hashCode => $mrjf($mrjc(
|
||||
recipe.hashCode, $mrjc(ingredient.hashCode, amountInGrams.hashCode)));
|
||||
@override
|
||||
bool operator ==(other) =>
|
||||
bool operator ==(dynamic other) =>
|
||||
identical(this, other) ||
|
||||
(other is IngredientInRecipe &&
|
||||
other.recipe == this.recipe &&
|
||||
|
@ -781,13 +794,13 @@ class $IngredientInRecipesTable extends IngredientInRecipes
|
|||
if (d.recipe.present) {
|
||||
context.handle(
|
||||
_recipeMeta, recipe.isAcceptableValue(d.recipe.value, _recipeMeta));
|
||||
} else if (recipe.isRequired && isInserting) {
|
||||
} else if (isInserting) {
|
||||
context.missing(_recipeMeta);
|
||||
}
|
||||
if (d.ingredient.present) {
|
||||
context.handle(_ingredientMeta,
|
||||
ingredient.isAcceptableValue(d.ingredient.value, _ingredientMeta));
|
||||
} else if (ingredient.isRequired && isInserting) {
|
||||
} else if (isInserting) {
|
||||
context.missing(_ingredientMeta);
|
||||
}
|
||||
if (d.amountInGrams.present) {
|
||||
|
@ -795,7 +808,7 @@ class $IngredientInRecipesTable extends IngredientInRecipes
|
|||
_amountInGramsMeta,
|
||||
amountInGrams.isAcceptableValue(
|
||||
d.amountInGrams.value, _amountInGramsMeta));
|
||||
} else if (amountInGrams.isRequired && isInserting) {
|
||||
} else if (isInserting) {
|
||||
context.missing(_amountInGramsMeta);
|
||||
}
|
||||
return context;
|
||||
|
@ -832,6 +845,7 @@ class $IngredientInRecipesTable extends IngredientInRecipes
|
|||
|
||||
abstract class _$Database extends GeneratedDatabase {
|
||||
_$Database(QueryExecutor e) : super(SqlTypeSystem.defaultInstance, e);
|
||||
_$Database.connect(DatabaseConnection c) : super.connect(c);
|
||||
$CategoriesTable _categories;
|
||||
$CategoriesTable get categories => _categories ??= $CategoriesTable(this);
|
||||
$RecipesTable _recipes;
|
||||
|
@ -881,7 +895,7 @@ class TotalWeightResult {
|
|||
@override
|
||||
int get hashCode => $mrjf($mrjc(title.hashCode, totalWeight.hashCode));
|
||||
@override
|
||||
bool operator ==(other) =>
|
||||
bool operator ==(dynamic other) =>
|
||||
identical(this, other) ||
|
||||
(other is TotalWeightResult &&
|
||||
other.title == this.title &&
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
/// with moor.
|
||||
library backends;
|
||||
|
||||
export 'src/runtime/components/component.dart' show SqlDialect;
|
||||
export 'src/runtime/executor/executor.dart';
|
||||
export 'src/runtime/executor/helpers/delegates.dart';
|
||||
export 'src/runtime/executor/helpers/engines.dart';
|
||||
export 'src/runtime/executor/helpers/results.dart';
|
||||
export 'src/runtime/query_builder/query_builder.dart' show SqlDialect;
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
/// Experimental bindings to the [json1](https://www.sqlite.org/json1.html)
|
||||
/// sqlite extension.
|
||||
///
|
||||
/// Note that the json1 extension might not be available on all runtimes. In
|
||||
/// particular, it can only work reliably on Android when using the
|
||||
/// [moor_ffi](https://moor.simonbinder.eu/docs/other-engines/vm/) and it might
|
||||
/// not work on older iOS versions.
|
||||
@experimental
|
||||
library json1;
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
import '../moor.dart';
|
||||
|
||||
/// Defines extensions on string expressions to support the json1 api from Dart.
|
||||
extension JsonExtensions on Expression<String, StringType> {
|
||||
/// Assuming that this string is a json array, returns the length of this json
|
||||
/// array.
|
||||
///
|
||||
/// The [path] parameter is optional. If it's set, it must refer to a valid
|
||||
/// path in this json that will be used instead of `this`. See the
|
||||
/// [sqlite documentation](https://www.sqlite.org/json1.html#path_arguments)
|
||||
/// for details. If [path] is an invalid path, this expression can cause an
|
||||
/// error when run by sqlite.
|
||||
///
|
||||
/// For this method to be valid, `this` must be a string representing a valid
|
||||
/// json array. Otherwise, sqlite will report an error when attempting to
|
||||
/// evaluate this expression.
|
||||
///
|
||||
/// See also:
|
||||
/// - the [sqlite documentation for this function](https://www.sqlite.org/json1.html#the_json_array_length_function)
|
||||
Expression<int, IntType> jsonArrayLength([String path]) {
|
||||
return FunctionCallExpression('json_array_length', [
|
||||
this,
|
||||
if (path != null) Variable.withString(path),
|
||||
]);
|
||||
}
|
||||
|
||||
/// Assuming that this string is a json object or array, extracts a part of
|
||||
/// this structure identified by [path].
|
||||
///
|
||||
/// For more details on how to format the [path] argument, see the
|
||||
/// [sqlite documentation](https://www.sqlite.org/json1.html#path_arguments).
|
||||
///
|
||||
/// Evaluating this expression will cause an error if [path] has an invalid
|
||||
/// format or `this` isn't well-formatted json.
|
||||
///
|
||||
/// Note that the [T] and [S] type parameters have to be set if this function
|
||||
/// is used in [JoinedSelectStatement.addColumns] or compared via
|
||||
/// [Expression.equals]. The [T] parameter denotes the mapped Dart type for
|
||||
/// this expression, such as [String]. THe [S] parameter denotes the mapper
|
||||
/// from moor that's responsible for mapping Dart objects to sqlite and vice
|
||||
/// versa. If [T] was set to [String], the matching value for [S] would be
|
||||
/// [StringType].
|
||||
Expression<T, S> jsonExtract<T, S extends SqlType<T>>(String path) {
|
||||
return FunctionCallExpression('json_extract', [
|
||||
this,
|
||||
Variable.withString(path),
|
||||
]).dartCast<T, S>();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
/// Contains utils to run moor databases in a background isolate. This API is
|
||||
/// not supported on the web.
|
||||
library isolate;
|
||||
|
||||
export 'src/runtime/isolate/moor_isolate.dart';
|
|
@ -7,32 +7,16 @@ export 'dart:typed_data' show Uint8List;
|
|||
// appropriate
|
||||
export 'package:meta/meta.dart' show required;
|
||||
|
||||
export 'package:moor/src/dsl/table.dart';
|
||||
export 'package:moor/src/dsl/columns.dart';
|
||||
export 'package:moor/src/dsl/database.dart';
|
||||
export 'package:moor/src/dsl/dsl.dart';
|
||||
export 'package:moor/src/runtime/query_builder/query_builder.dart';
|
||||
|
||||
export 'package:moor/src/runtime/components/join.dart'
|
||||
show innerJoin, leftOuterJoin, crossJoin;
|
||||
export 'package:moor/src/runtime/components/limit.dart';
|
||||
export 'package:moor/src/runtime/components/order_by.dart';
|
||||
export 'package:moor/src/runtime/executor/executor.dart';
|
||||
export 'package:moor/src/types/type_system.dart';
|
||||
export 'package:moor/src/runtime/expressions/comparable.dart';
|
||||
export 'package:moor/src/runtime/expressions/user_api.dart';
|
||||
export 'package:moor/src/runtime/executor/transactions.dart';
|
||||
export 'package:moor/src/runtime/statements/query.dart';
|
||||
export 'package:moor/src/runtime/statements/select.dart';
|
||||
export 'package:moor/src/runtime/statements/update.dart';
|
||||
export 'package:moor/src/runtime/statements/insert.dart';
|
||||
export 'package:moor/src/runtime/statements/delete.dart';
|
||||
export 'package:moor/src/runtime/structure/columns.dart';
|
||||
export 'package:moor/src/runtime/structure/entities.dart';
|
||||
export 'package:moor/src/runtime/structure/error_handling.dart';
|
||||
export 'package:moor/src/runtime/structure/table_info.dart';
|
||||
export 'package:moor/src/runtime/data_verification.dart';
|
||||
export 'package:moor/src/runtime/data_class.dart';
|
||||
export 'package:moor/src/runtime/database.dart';
|
||||
export 'package:moor/src/types/sql_types.dart';
|
||||
export 'package:moor/src/runtime/migration.dart';
|
||||
export 'package:moor/src/runtime/api/runtime_api.dart';
|
||||
export 'package:moor/src/runtime/types/sql_types.dart'
|
||||
hide ComparableType, Monoid, FullArithmetic;
|
||||
export 'package:moor/src/runtime/exceptions.dart';
|
||||
export 'package:moor/src/utils/expand_variables.dart';
|
||||
export 'package:moor/src/utils/hash.dart';
|
||||
|
|
|
@ -1,43 +1,31 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:moor/moor.dart';
|
||||
import 'package:moor/src/runtime/expressions/expression.dart';
|
||||
part of 'dsl.dart';
|
||||
|
||||
/// Base class for columns in sql. The [T] type refers to the type a value of
|
||||
/// this column will have in Dart, [S] is the mapping class from moor.
|
||||
abstract class Column<T, S extends SqlType<T>> extends Expression<T, S> {}
|
||||
abstract class Column<T, S extends SqlType<T>> extends Expression<T, S> {
|
||||
@override
|
||||
final Precedence precedence = Precedence.primary;
|
||||
}
|
||||
|
||||
/// A column that stores int values.
|
||||
abstract class IntColumn extends Column<int, IntType> implements IntExpression {
|
||||
}
|
||||
abstract class IntColumn extends Column<int, IntType> {}
|
||||
|
||||
/// A column that stores boolean values. Booleans will be stored as an integer
|
||||
/// that can either be 0 (false) or 1 (true).
|
||||
abstract class BoolColumn extends Column<bool, BoolType> {}
|
||||
|
||||
/// A column that stores text.
|
||||
abstract class TextColumn extends Column<String, StringType> {
|
||||
/// Whether this column matches the given pattern. For details on what patters
|
||||
/// are valid and how they are interpreted, check out
|
||||
/// [this tutorial](http://www.sqlitetutorial.net/sqlite-like/).
|
||||
Expression<bool, BoolType> like(String regex);
|
||||
|
||||
/// Uses the given [collate] sequence when comparing this column to other
|
||||
/// values.
|
||||
Expression<String, StringType> collate(Collate collate);
|
||||
}
|
||||
abstract class TextColumn extends Column<String, StringType> {}
|
||||
|
||||
/// A column that stores a [DateTime]. Times will be stored as unix timestamp
|
||||
/// and will thus have a second accuracy.
|
||||
abstract class DateTimeColumn extends Column<DateTime, DateTimeType>
|
||||
implements DateTimeExpression {}
|
||||
abstract class DateTimeColumn extends Column<DateTime, DateTimeType> {}
|
||||
|
||||
/// A column that stores arbitrary blobs of data as a [Uint8List].
|
||||
abstract class BlobColumn extends Column<Uint8List, BlobType> {}
|
||||
|
||||
/// A column that stores floating point numeric values.
|
||||
abstract class RealColumn extends Column<double, RealType>
|
||||
implements DoubleExpression {}
|
||||
abstract class RealColumn extends Column<double, RealType> {}
|
||||
|
||||
/// A column builder is used to specify which columns should appear in a table.
|
||||
/// All of the methods defined in this class and its subclasses are not meant to
|
||||
|
@ -60,11 +48,11 @@ class ColumnBuilder<
|
|||
/// Note that using [named] __does not__ have an effect on the json key of an
|
||||
/// object. To change the json key, annotate this column getter with
|
||||
/// [JsonKey].
|
||||
Builder named(String name) => null;
|
||||
Builder named(String name) => _isGenerated();
|
||||
|
||||
/// Marks this column as nullable. Nullable columns should not appear in a
|
||||
/// primary key. Columns are non-null by default.
|
||||
Builder nullable() => null;
|
||||
Builder nullable() => _isGenerated();
|
||||
|
||||
/// Tells moor to write a custom constraint after this column definition when
|
||||
/// writing this column, for instance in a CREATE TABLE statement.
|
||||
|
@ -91,15 +79,16 @@ class ColumnBuilder<
|
|||
/// See also:
|
||||
/// - https://www.sqlite.org/syntax/column-constraint.html
|
||||
/// - [GeneratedColumn.writeCustomConstraints]
|
||||
Builder customConstraint(String constraint) => null;
|
||||
Builder customConstraint(String constraint) => _isGenerated();
|
||||
|
||||
/// The column will use this expression when a row is inserted and no value
|
||||
/// has been specified.
|
||||
///
|
||||
/// Note: Unless most other methods used to declare tables, the parameter
|
||||
/// [e] which denotes the default expression, doesn't have to be constant.
|
||||
/// Particularly, you can use methods like [and], [or] and [not] to form
|
||||
/// expressions here.
|
||||
/// [e] which denotes the default expression, doesn't have to be a Dart
|
||||
/// constant.
|
||||
/// Particularly, you can use operators like those defined in
|
||||
/// [BooleanExpressionOperators] to form expressions here.
|
||||
///
|
||||
/// If you need a column that just stores a static default value, you could
|
||||
/// use this method with a [Constant]:
|
||||
|
@ -112,7 +101,35 @@ class ColumnBuilder<
|
|||
/// TABLE statements.
|
||||
/// - [currentDate] and [currentDateAndTime], which are useful expressions to
|
||||
/// store the current date/time as a default value.
|
||||
Builder withDefault(Expression<ResultDartType, ResultSqlType> e) => null;
|
||||
Builder withDefault(Expression<ResultDartType, ResultSqlType> e) =>
|
||||
_isGenerated();
|
||||
|
||||
/// Sets a dynamic default value for this column.
|
||||
///
|
||||
/// When a row is inserted into the table and no value has been specified for
|
||||
/// this column, [onInsert] will be evaluated. Its return value will be used
|
||||
/// for the missing column. [onInsert] may return different values when called
|
||||
/// multiple times.
|
||||
///
|
||||
/// Here's an example using the [uuid](https://pub.dev/packages/uuid) package:
|
||||
///
|
||||
/// ```dart
|
||||
/// final uuid = Uuid();
|
||||
///
|
||||
/// class Pictures extends Table {
|
||||
/// TextColumn get id => text().clientDefault(() => uuid.v4())();
|
||||
/// BlobColumn get rawData => blob();
|
||||
///
|
||||
/// @override
|
||||
/// Set<Column> get primaryKey = {id};
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// For a default value that's constant, it is more efficient to use
|
||||
/// [withDefault] instead. [withDefault] will write the default value into the
|
||||
/// generated `CREATE TABLE` statement. The underlying sql engine will then
|
||||
/// apply the default value.
|
||||
Builder clientDefault(ResultDartType Function() onInsert) => _isGenerated();
|
||||
|
||||
/// Uses a custom [converter] to store custom Dart objects in a single column
|
||||
/// and automatically mapping them from and to sql.
|
||||
|
@ -150,12 +167,12 @@ class ColumnBuilder<
|
|||
/// ```
|
||||
/// The generated row class will then use a `MyFancyClass` instead of a
|
||||
/// `String`, which would usually be used for [Table.text] columns.
|
||||
Builder map<T>(TypeConverter<T, ResultDartType> converter) => null;
|
||||
Builder map<T>(TypeConverter<T, ResultDartType> converter) => _isGenerated();
|
||||
|
||||
/// Turns this column builder into a column. This method won't actually be
|
||||
/// called in your code. Instead, moor_generator will take a look at your
|
||||
/// source code to figure out your table structure.
|
||||
ResultColumn call() => null;
|
||||
ResultColumn call() => _isGenerated();
|
||||
}
|
||||
|
||||
/// Tells the generator to build an [IntColumn]. See the docs at [ColumnBuilder]
|
||||
|
@ -164,7 +181,7 @@ class IntColumnBuilder
|
|||
extends ColumnBuilder<IntColumnBuilder, IntColumn, IntType, int> {
|
||||
/// Enables auto-increment for this column, which will also make this column
|
||||
/// the primary key of the table.
|
||||
IntColumnBuilder autoIncrement() => this;
|
||||
IntColumnBuilder autoIncrement() => _isGenerated();
|
||||
}
|
||||
|
||||
/// Tells the generator to build an [BoolColumn]. See the docs at
|
||||
|
@ -192,7 +209,7 @@ class TextColumnBuilder
|
|||
/// string so that [String.length] is smaller than [min], the query will throw
|
||||
/// an exception when executed and no data will be written. The same applies
|
||||
/// for [max].
|
||||
TextColumnBuilder withLength({int min, int max}) => this;
|
||||
TextColumnBuilder withLength({int min, int max}) => _isGenerated();
|
||||
}
|
||||
|
||||
/// Tells the generator to build an [DateTimeColumn]. See the docs at
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import 'package:meta/meta.dart';
|
||||
import 'package:moor/moor.dart';
|
||||
part of 'dsl.dart';
|
||||
|
||||
/// Use this class as an annotation to inform moor_generator that a database
|
||||
/// class should be generated using the specified [UseMoor.tables].
|
||||
|
@ -11,7 +10,8 @@ import 'package:moor/moor.dart';
|
|||
/// a [QueryEngine] to use moor:
|
||||
/// ```dart
|
||||
/// class MyDatabase extends _$MyDatabase { // _$MyDatabase was generated
|
||||
/// MyDatabase() : super(FlutterQueryExecutor.inDatabaseFolder(path: 'path.db'));
|
||||
/// MyDatabase():
|
||||
/// super(FlutterQueryExecutor.inDatabaseFolder(path: 'path.db'));
|
||||
/// }
|
||||
/// ```
|
||||
class UseMoor {
|
||||
|
@ -48,10 +48,7 @@ class UseMoor {
|
|||
/// Defines the `.moor` files to include when building the table structure for
|
||||
/// this database. For details on how to integrate `.moor` files into your
|
||||
/// Dart code, see [the documentation](https://moor.simonbinder.eu/docs/using-sql/custom_tables/).
|
||||
///
|
||||
/// Please note that this feature is experimental at the moment.
|
||||
/// {@endtemplate}
|
||||
@experimental
|
||||
final Set<String> include;
|
||||
|
||||
/// Use this class as an annotation to inform moor_generator that a database
|
||||
|
@ -60,7 +57,7 @@ class UseMoor {
|
|||
@required this.tables,
|
||||
this.daos = const [],
|
||||
this.queries = const {},
|
||||
@experimental this.include = const {},
|
||||
this.include = const {},
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -93,7 +90,6 @@ class UseDao {
|
|||
final Map<String, String> queries;
|
||||
|
||||
/// {@macro moor_include_param}
|
||||
@experimental
|
||||
final Set<String> include;
|
||||
|
||||
/// Annotation for a class to declare it as an dao. See [UseDao] and the
|
||||
|
@ -101,5 +97,5 @@ class UseDao {
|
|||
const UseDao(
|
||||
{@required this.tables,
|
||||
this.queries = const {},
|
||||
@experimental this.include = const {}});
|
||||
this.include = const {}});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
import 'dart:typed_data' show Uint8List;
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:moor/moor.dart';
|
||||
|
||||
part 'columns.dart';
|
||||
part 'database.dart';
|
||||
part 'table.dart';
|
||||
|
||||
/// Implementation for dsl methods that aren't called at runtime but only exist
|
||||
/// for the generator to pick up. For instance, in
|
||||
/// ```dart
|
||||
/// class MyTable extends Table {
|
||||
/// IntColumn get id => integer().autoIncrement()();
|
||||
/// }
|
||||
/// ```
|
||||
/// Neither [Table.integer], [IntColumnBuilder.autoIncrement] or
|
||||
/// [ColumnBuilder.call] will be called at runtime. Instead, the generator will
|
||||
/// take a look at the written Dart code to recognize that `id` is a column of
|
||||
/// type int that has auto increment (and is thus the primary key). It will
|
||||
/// generate a subclass of `MyTable` which looks like this:
|
||||
/// ```dart
|
||||
/// class _$MyTable extends MyTable {
|
||||
/// IntColumn get id => GeneratedIntColumn(
|
||||
/// 'id',
|
||||
/// 'my-table',
|
||||
/// false,
|
||||
/// declaredAsPrimaryKey: false,
|
||||
/// declaredAsAutoIncrement: true,
|
||||
/// );
|
||||
/// }
|
||||
/// ```
|
||||
Null /* = Never */ _isGenerated() {
|
||||
throw UnsupportedError(
|
||||
'This method should not be called at runtime. Are you sure you re-ran the '
|
||||
'builder after changing your tables or databases?',
|
||||
);
|
||||
}
|
|
@ -1,6 +1,4 @@
|
|||
import 'dart:typed_data' show Uint8List;
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:moor/moor.dart';
|
||||
part of 'dsl.dart';
|
||||
|
||||
/// Subclasses represent a table in a database generated by moor.
|
||||
abstract class Table {
|
||||
|
@ -62,7 +60,7 @@ abstract class Table {
|
|||
/// IntColumn get id => integer().autoIncrement()();
|
||||
/// ```
|
||||
@protected
|
||||
IntColumnBuilder integer() => null;
|
||||
IntColumnBuilder integer() => _isGenerated();
|
||||
|
||||
/// Use this as the body of a getter to declare a column that holds strings.
|
||||
/// Example (inside the body of a table class):
|
||||
|
@ -70,7 +68,7 @@ abstract class Table {
|
|||
/// TextColumn get name => text()();
|
||||
/// ```
|
||||
@protected
|
||||
TextColumnBuilder text() => null;
|
||||
TextColumnBuilder text() => _isGenerated();
|
||||
|
||||
/// Use this as the body of a getter to declare a column that holds bools.
|
||||
/// Example (inside the body of a table class):
|
||||
|
@ -78,16 +76,16 @@ abstract class Table {
|
|||
/// BoolColumn get isAwesome => boolean()();
|
||||
/// ```
|
||||
@protected
|
||||
BoolColumnBuilder boolean() => null;
|
||||
BoolColumnBuilder boolean() => _isGenerated();
|
||||
|
||||
/// Use this as the body of a getter to declare a column that holds date and
|
||||
/// time.
|
||||
/// time. Note that [DateTime] values are stored on a second-accuracy.
|
||||
/// Example (inside the body of a table class):
|
||||
/// ```
|
||||
/// DateTimeColumn get accountCreatedAt => dateTime()();
|
||||
/// ```
|
||||
@protected
|
||||
DateTimeColumnBuilder dateTime() => null;
|
||||
DateTimeColumnBuilder dateTime() => _isGenerated();
|
||||
|
||||
/// Use this as the body of a getter to declare a column that holds arbitrary
|
||||
/// data blobs, stored as an [Uint8List]. Example:
|
||||
|
@ -95,7 +93,7 @@ abstract class Table {
|
|||
/// BlobColumn get payload => blob()();
|
||||
/// ```
|
||||
@protected
|
||||
BlobColumnBuilder blob() => null;
|
||||
BlobColumnBuilder blob() => _isGenerated();
|
||||
|
||||
/// Use this as the body of a getter to declare a column that holds floating
|
||||
/// point numbers. Example
|
||||
|
@ -103,7 +101,7 @@ abstract class Table {
|
|||
/// RealColumn get averageSpeed => real()();
|
||||
/// ```
|
||||
@protected
|
||||
RealColumnBuilder real() => null;
|
||||
RealColumnBuilder real() => _isGenerated();
|
||||
}
|
||||
|
||||
/// A class to to be used as an annotation on [Table] classes to customize the
|
||||
|
|
|
@ -0,0 +1,131 @@
|
|||
part of 'runtime_api.dart';
|
||||
|
||||
/// Contains operations to run queries in a batched mode. This can be much more
|
||||
/// efficient when running a lot of similar queries at the same time, making
|
||||
/// this api suitable for bulk updates.
|
||||
class Batch {
|
||||
final Map<String, List<List<dynamic>>> _createdStatements = {};
|
||||
final QueryEngine _engine;
|
||||
|
||||
/// Whether we should start a transaction when completing.
|
||||
final bool _startTransaction;
|
||||
|
||||
final Set<TableInfo> _affectedTables = {};
|
||||
|
||||
Batch._(this._engine, this._startTransaction);
|
||||
|
||||
/// Inserts a row constructed from the fields in [row].
|
||||
///
|
||||
/// All fields in the entity that don't have a default value or auto-increment
|
||||
/// must be set and non-null. Otherwise, an [InvalidDataException] will be
|
||||
/// thrown.
|
||||
///
|
||||
/// By default, an exception will be thrown if another row with the same
|
||||
/// primary key already exists. This behavior can be overridden with [mode],
|
||||
/// for instance by using [InsertMode.replace] or [InsertMode.insertOrIgnore].
|
||||
///
|
||||
/// See also:
|
||||
/// - [InsertStatement.insert], which would be used outside a [Batch].
|
||||
void insert<D extends DataClass>(TableInfo<Table, D> table, Insertable<D> row,
|
||||
{InsertMode mode}) {
|
||||
_affectedTables.add(table);
|
||||
final actualMode = mode ?? InsertMode.insert;
|
||||
final context =
|
||||
InsertStatement<D>(_engine, table).createContext(row, actualMode);
|
||||
_addContext(context);
|
||||
}
|
||||
|
||||
/// Inserts all [rows] into the [table].
|
||||
///
|
||||
/// All fields in a row that don't have a default value or auto-increment
|
||||
/// must be set and non-null. Otherwise, an [InvalidDataException] will be
|
||||
/// thrown.
|
||||
/// By default, an exception will be thrown if another row with the same
|
||||
/// primary key already exists. This behavior can be overridden with [mode],
|
||||
/// for instance by using [InsertMode.replace] or [InsertMode.insertOrIgnore].
|
||||
/// Using [insertAll] will not disable primary keys or any column constraint
|
||||
/// checks.
|
||||
void insertAll<D extends DataClass>(
|
||||
TableInfo<Table, D> table, List<Insertable<D>> rows,
|
||||
{InsertMode mode}) {
|
||||
for (final row in rows) {
|
||||
insert<D>(table, row, mode: mode);
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes all present columns from the [row] into all rows in the [table]
|
||||
/// that match the [where] clause.
|
||||
///
|
||||
/// For more details on how updates work in moor, check out
|
||||
/// [UpdateStatement.write] or the [documentation with examples](https://moor.simonbinder.eu/docs/getting-started/writing_queries/#updates-and-deletes)
|
||||
void update<T extends Table, D extends DataClass>(
|
||||
TableInfo<T, D> table, Insertable<D> row,
|
||||
{Expression<bool, BoolType> Function(T table) where}) {
|
||||
_affectedTables.add(table);
|
||||
final stmt = UpdateStatement(_engine, table);
|
||||
if (where != null) stmt.where(where);
|
||||
|
||||
stmt.write(row, dontExecute: true);
|
||||
final context = stmt.constructQuery();
|
||||
_addContext(context);
|
||||
}
|
||||
|
||||
/// Replaces the [row] from the [table] with the updated values. The row in
|
||||
/// the table with the same primary key will be replaced.
|
||||
///
|
||||
/// See also:
|
||||
/// - [UpdateStatement.replace], which is what would be used outside of a
|
||||
/// [Batch].
|
||||
void replace<T extends Table, D extends DataClass>(
|
||||
TableInfo<T, D> table,
|
||||
Insertable<D> row,
|
||||
) {
|
||||
_affectedTables.add(table);
|
||||
final stmt = UpdateStatement(_engine, table)
|
||||
..replace(row, dontExecute: true);
|
||||
_addContext(stmt.constructQuery());
|
||||
}
|
||||
|
||||
/// Helper that calls [replace] for all [rows].
|
||||
void replaceAll<T extends Table, D extends DataClass>(
|
||||
TableInfo<T, D> table, List<Insertable<D>> rows) {
|
||||
for (final row in rows) {
|
||||
replace(table, row);
|
||||
}
|
||||
}
|
||||
|
||||
void _addContext(GenerationContext ctx) {
|
||||
final sql = ctx.sql;
|
||||
final variableSet = _createdStatements.putIfAbsent(sql, () => []);
|
||||
variableSet.add(ctx.boundVariables);
|
||||
}
|
||||
|
||||
Future<void> _commit() async {
|
||||
await _engine.executor.ensureOpen();
|
||||
|
||||
if (_startTransaction) {
|
||||
TransactionExecutor transaction;
|
||||
|
||||
try {
|
||||
transaction = _engine.executor.beginTransaction();
|
||||
await transaction.doWhenOpened(_runWith);
|
||||
await transaction.send();
|
||||
} catch (e) {
|
||||
await transaction.rollback();
|
||||
rethrow;
|
||||
}
|
||||
} else {
|
||||
await _runWith(_engine.executor);
|
||||
}
|
||||
|
||||
_engine.markTablesUpdated(_affectedTables);
|
||||
}
|
||||
|
||||
Future<void> _runWith(QueryExecutor executor) async {
|
||||
final statements = _createdStatements.entries.map((entry) {
|
||||
return BatchedStatement(entry.key, entry.value);
|
||||
}).toList();
|
||||
|
||||
await executor.runBatched(statements);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
part of 'runtime_api.dart';
|
||||
|
||||
/// A database connection managed by moor. Contains three components:
|
||||
/// - a [SqlTypeSystem], which is responsible to map between Dart types and
|
||||
/// values understood by the database engine.
|
||||
/// - a [QueryExecutor], which runs sql commands
|
||||
/// - a [StreamQueryStore], which dispatches table changes to listening queries,
|
||||
/// on which the auto-updating queries are based.
|
||||
class DatabaseConnection {
|
||||
/// The type system to use with this database. The type system is responsible
|
||||
/// for mapping Dart objects into sql expressions and vice-versa.
|
||||
final SqlTypeSystem typeSystem;
|
||||
|
||||
/// The executor to use when queries are executed.
|
||||
final QueryExecutor executor;
|
||||
|
||||
/// Manages active streams from select statements.
|
||||
final StreamQueryStore streamQueries;
|
||||
|
||||
/// Constructs a raw database connection from the three components.
|
||||
DatabaseConnection(this.typeSystem, this.executor, this.streamQueries);
|
||||
|
||||
/// Constructs a [DatabaseConnection] from the [QueryExecutor] by using the
|
||||
/// default type system and a new [StreamQueryStore].
|
||||
DatabaseConnection.fromExecutor(this.executor)
|
||||
: typeSystem = SqlTypeSystem.defaultInstance,
|
||||
streamQueries = StreamQueryStore();
|
||||
}
|
||||
|
||||
/// Manages a [DatabaseConnection] to send queries to the database.
|
||||
abstract class DatabaseConnectionUser {
|
||||
/// The database connection used by this [DatabaseConnectionUser].
|
||||
@protected
|
||||
final DatabaseConnection connection;
|
||||
|
||||
/// The type system to use with this database. The type system is responsible
|
||||
/// for mapping Dart objects into sql expressions and vice-versa.
|
||||
SqlTypeSystem get typeSystem => connection.typeSystem;
|
||||
|
||||
/// The executor to use when queries are executed.
|
||||
QueryExecutor get executor => connection.executor;
|
||||
|
||||
/// Manages active streams from select statements.
|
||||
@visibleForTesting
|
||||
@protected
|
||||
StreamQueryStore get streamQueries => connection.streamQueries;
|
||||
|
||||
/// Constructs a database connection user, which is responsible to store query
|
||||
/// streams, wrap the underlying executor and perform type mapping.
|
||||
DatabaseConnectionUser(SqlTypeSystem typeSystem, QueryExecutor executor,
|
||||
{StreamQueryStore streamQueries})
|
||||
: connection = DatabaseConnection(
|
||||
typeSystem, executor, streamQueries ?? StreamQueryStore());
|
||||
|
||||
/// Creates another [DatabaseConnectionUser] by referencing the implementation
|
||||
/// from the [other] user.
|
||||
DatabaseConnectionUser.delegate(DatabaseConnectionUser other,
|
||||
{SqlTypeSystem typeSystem,
|
||||
QueryExecutor executor,
|
||||
StreamQueryStore streamQueries})
|
||||
: connection = DatabaseConnection(
|
||||
typeSystem ?? other.connection.typeSystem,
|
||||
executor ?? other.connection.executor,
|
||||
streamQueries ?? other.connection.streamQueries,
|
||||
);
|
||||
|
||||
/// Constructs a [DatabaseConnectionUser] that will use the provided
|
||||
/// [DatabaseConnection].
|
||||
DatabaseConnectionUser.fromConnection(this.connection);
|
||||
|
||||
/// Marks the tables as updated. This method will be called internally
|
||||
/// whenever a update, delete or insert statement is issued on the database.
|
||||
/// We can then inform all active select-streams on those tables that their
|
||||
/// snapshot might be out-of-date and needs to be fetched again.
|
||||
void markTablesUpdated(Set<TableInfo> tables) {
|
||||
streamQueries.handleTableUpdates(tables);
|
||||
}
|
||||
|
||||
/// Creates and auto-updating stream from the given select statement. This
|
||||
/// method should not be used directly.
|
||||
Stream<T> createStream<T>(QueryStreamFetcher<T> stmt) =>
|
||||
streamQueries.registerStream(stmt);
|
||||
|
||||
/// Creates a copy of the table with an alias so that it can be used in the
|
||||
/// same query more than once.
|
||||
///
|
||||
/// Example which uses the same table (here: points) more than once to
|
||||
/// differentiate between the start and end point of a route:
|
||||
/// ```
|
||||
/// var source = alias(points, 'source');
|
||||
/// var destination = alias(points, 'dest');
|
||||
///
|
||||
/// select(routes).join([
|
||||
/// innerJoin(source, routes.startPoint.equalsExp(source.id)),
|
||||
/// innerJoin(destination, routes.startPoint.equalsExp(destination.id)),
|
||||
/// ]);
|
||||
/// ```
|
||||
T alias<T extends Table, D extends DataClass>(
|
||||
TableInfo<T, D> table, String alias) {
|
||||
return table.createAlias(alias).asDslTable;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
part of 'runtime_api.dart';
|
||||
|
||||
/// Class that runs queries to a subset of all available queries in a database.
|
||||
///
|
||||
/// This comes in handy to structure large amounts of database code better: The
|
||||
/// migration logic can live in the main [GeneratedDatabase] class, but code
|
||||
/// can be extracted into [DatabaseAccessor]s outside of that database.
|
||||
/// For details on how to write a dao, see [UseDao].
|
||||
/// [T] should be the associated database class you wrote.
|
||||
abstract class DatabaseAccessor<T extends GeneratedDatabase>
|
||||
extends DatabaseConnectionUser with QueryEngine {
|
||||
@override
|
||||
final bool topLevel = true;
|
||||
|
||||
/// The main database instance for this dao
|
||||
@protected
|
||||
final T db;
|
||||
|
||||
/// Used internally by moor
|
||||
DatabaseAccessor(this.db) : super.delegate(db);
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
part of 'runtime_api.dart';
|
||||
|
||||
/// Keep track of how many databases have been opened for a given database
|
||||
/// type.
|
||||
/// We get a number of error reports of "moor not generating tables" that have
|
||||
/// their origin in users opening multiple instances of their database. This
|
||||
/// can cause a race conditions when the second [GeneratedDatabase] is opening a
|
||||
/// underlying [DatabaseConnection] that is already opened but doesn't have the
|
||||
/// tables created.
|
||||
Map<Type, int> _openedDbCount = {};
|
||||
|
||||
/// A base class for all generated databases.
|
||||
abstract class GeneratedDatabase extends DatabaseConnectionUser
|
||||
with QueryEngine {
|
||||
@override
|
||||
final bool topLevel = true;
|
||||
|
||||
/// Specify the schema version of your database. Whenever you change or add
|
||||
/// tables, you should bump this field and provide a [migration] strategy.
|
||||
int get schemaVersion;
|
||||
|
||||
/// Defines the migration strategy that will determine how to deal with an
|
||||
/// increasing [schemaVersion]. The default value only supports creating the
|
||||
/// database by creating all tables known in this database. When you have
|
||||
/// changes in your schema, you'll need a custom migration strategy to create
|
||||
/// the new tables or change the columns.
|
||||
MigrationStrategy get migration => MigrationStrategy();
|
||||
MigrationStrategy _cachedMigration;
|
||||
MigrationStrategy get _resolvedMigration => _cachedMigration ??= migration;
|
||||
|
||||
/// A list of tables specified in this database.
|
||||
List<TableInfo> get allTables;
|
||||
|
||||
/// A [Type] can't be sent across isolates. Instances of this class shouldn't
|
||||
/// be sent over isolates either, so let's keep a reference to a [Type] that
|
||||
/// definitely prohibits this.
|
||||
// ignore: unused_field
|
||||
final Type _$dontSendThisOverIsolates = Null;
|
||||
|
||||
/// Used by generated code
|
||||
GeneratedDatabase(SqlTypeSystem types, QueryExecutor executor,
|
||||
{StreamQueryStore streamStore})
|
||||
: super(types, executor, streamQueries: streamStore) {
|
||||
executor?.databaseInfo = this;
|
||||
assert(_handleInstantiated());
|
||||
}
|
||||
|
||||
/// Used by generated code to connect to a database that is already open.
|
||||
GeneratedDatabase.connect(DatabaseConnection connection)
|
||||
: super.fromConnection(connection) {
|
||||
connection?.executor?.databaseInfo = this;
|
||||
assert(_handleInstantiated());
|
||||
}
|
||||
|
||||
bool _handleInstantiated() {
|
||||
if (!_openedDbCount.containsKey(runtimeType) ||
|
||||
moorRuntimeOptions.dontWarnAboutMultipleDatabases) {
|
||||
_openedDbCount[runtimeType] = 1;
|
||||
return true;
|
||||
}
|
||||
final count = ++_openedDbCount[runtimeType];
|
||||
if (count > 1) {
|
||||
// ignore: avoid_print
|
||||
print(
|
||||
'WARNING (moor): It looks like you\'ve created the database class'
|
||||
'$runtimeType multiple times. When these two databases use the same '
|
||||
'QueryExecutor, race conditions will ocur and might corrupt the '
|
||||
'database. \n'
|
||||
'Try to follow the advice at https://moor.simonbinder.eu/faq/#using-the-database '
|
||||
'or, if you know what you\'re doing, set '
|
||||
'moorRuntimeOptions.dontWarnAboutMultipleDatabases = true\n'
|
||||
'Here is the stacktrace from when the database was opened a second '
|
||||
'time:\n${StackTrace.current}\n'
|
||||
'This warning will only appear on debug builds.',
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Creates a [Migrator] with the provided query executor. Migrators generate
|
||||
/// sql statements to create or drop tables.
|
||||
///
|
||||
/// This api is mainly used internally in moor, for instance in
|
||||
/// [handleDatabaseCreation] and [handleDatabaseVersionChange]. However, it
|
||||
/// can also be used if you need to create tables manually and outside of a
|
||||
/// [MigrationStrategy]. For almost all use cases, overriding [migration]
|
||||
/// should suffice.
|
||||
@protected
|
||||
Migrator createMigrator([SqlExecutor executor]) {
|
||||
final actualExecutor = executor ?? customStatement;
|
||||
return Migrator(this, actualExecutor);
|
||||
}
|
||||
|
||||
/// Handles database creation by delegating the work to the [migration]
|
||||
/// strategy. This method should not be called by users.
|
||||
Future<void> handleDatabaseCreation({@required SqlExecutor executor}) {
|
||||
final migrator = createMigrator(executor);
|
||||
return _resolvedMigration.onCreate(migrator);
|
||||
}
|
||||
|
||||
/// Handles database updates by delegating the work to the [migration]
|
||||
/// strategy. This method should not be called by users.
|
||||
Future<void> handleDatabaseVersionChange(
|
||||
{@required SqlExecutor executor, int from, int to}) {
|
||||
final migrator = createMigrator(executor);
|
||||
return _resolvedMigration.onUpgrade(migrator, from, to);
|
||||
}
|
||||
|
||||
/// Handles the before opening callback as set in the [migration]. This method
|
||||
/// is used internally by database implementations and should not be called by
|
||||
/// users.
|
||||
Future<void> beforeOpenCallback(
|
||||
QueryExecutor executor, OpeningDetails details) {
|
||||
final migration = _resolvedMigration;
|
||||
|
||||
if (migration.beforeOpen != null) {
|
||||
return _runEngineZoned(
|
||||
BeforeOpenRunner(this, executor),
|
||||
() => migration.beforeOpen(details),
|
||||
);
|
||||
}
|
||||
return Future.value();
|
||||
}
|
||||
|
||||
/// Closes this database and releases associated resources.
|
||||
Future<void> close() async {
|
||||
await executor.close();
|
||||
|
||||
if (_openedDbCount[runtimeType] != null) {
|
||||
_openedDbCount[runtimeType]--;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,110 +1,17 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:moor/moor.dart';
|
||||
import 'package:moor/src/runtime/components/component.dart';
|
||||
import 'package:moor/src/runtime/executor/before_open.dart';
|
||||
import 'package:moor/src/runtime/executor/stream_queries.dart';
|
||||
import 'package:moor/src/types/type_system.dart';
|
||||
import 'package:moor/src/runtime/statements/delete.dart';
|
||||
import 'package:moor/src/runtime/statements/select.dart';
|
||||
import 'package:moor/src/runtime/statements/update.dart';
|
||||
part of 'runtime_api.dart';
|
||||
|
||||
const _zoneRootUserKey = #DatabaseConnectionUser;
|
||||
|
||||
typedef _CustomWriter<T> = Future<T> Function(
|
||||
QueryExecutor e, String sql, List<dynamic> vars);
|
||||
|
||||
/// Class that runs queries to a subset of all available queries in a database.
|
||||
///
|
||||
/// This comes in handy to structure large amounts of database code better: The
|
||||
/// migration logic can live in the main [GeneratedDatabase] class, but code
|
||||
/// can be extracted into [DatabaseAccessor]s outside of that database.
|
||||
/// For details on how to write a dao, see [UseDao].
|
||||
abstract class DatabaseAccessor<T extends GeneratedDatabase>
|
||||
extends DatabaseConnectionUser with QueryEngine {
|
||||
@override
|
||||
final bool topLevel = true;
|
||||
|
||||
/// The main database instance for this dao
|
||||
@protected
|
||||
final T db;
|
||||
|
||||
/// Used internally by moor
|
||||
DatabaseAccessor(this.db) : super.delegate(db);
|
||||
}
|
||||
|
||||
/// Manages a [QueryExecutor] and optionally an own [SqlTypeSystem] or
|
||||
/// [StreamQueryStore] to send queries to the database.
|
||||
abstract class DatabaseConnectionUser {
|
||||
/// The type system to use with this database. The type system is responsible
|
||||
/// for mapping Dart objects into sql expressions and vice-versa.
|
||||
final SqlTypeSystem typeSystem;
|
||||
|
||||
/// The executor to use when queries are executed.
|
||||
final QueryExecutor executor;
|
||||
|
||||
/// Manages active streams from select statements.
|
||||
@visibleForTesting
|
||||
@protected
|
||||
StreamQueryStore streamQueries;
|
||||
|
||||
/// Constructs a database connection user, which is responsible to store query
|
||||
/// streams, wrap the underlying executor and perform type mapping.
|
||||
DatabaseConnectionUser(this.typeSystem, this.executor, {this.streamQueries}) {
|
||||
streamQueries ??= StreamQueryStore();
|
||||
}
|
||||
|
||||
/// Creates another [DatabaseConnectionUser] by referencing the implementation
|
||||
/// from the [other] user.
|
||||
DatabaseConnectionUser.delegate(DatabaseConnectionUser other,
|
||||
{SqlTypeSystem typeSystem,
|
||||
QueryExecutor executor,
|
||||
StreamQueryStore streamQueries})
|
||||
: typeSystem = typeSystem ?? other.typeSystem,
|
||||
executor = executor ?? other.executor,
|
||||
streamQueries = streamQueries ?? other.streamQueries;
|
||||
|
||||
/// Marks the tables as updated. This method will be called internally
|
||||
/// whenever a update, delete or insert statement is issued on the database.
|
||||
/// We can then inform all active select-streams on those tables that their
|
||||
/// snapshot might be out-of-date and needs to be fetched again.
|
||||
void markTablesUpdated(Set<TableInfo> tables) {
|
||||
streamQueries.handleTableUpdates(tables);
|
||||
}
|
||||
|
||||
/// Creates and auto-updating stream from the given select statement. This
|
||||
/// method should not be used directly.
|
||||
Stream<T> createStream<T>(QueryStreamFetcher<T> stmt) =>
|
||||
streamQueries.registerStream(stmt);
|
||||
|
||||
/// Creates a copy of the table with an alias so that it can be used in the
|
||||
/// same query more than once.
|
||||
///
|
||||
/// Example which uses the same table (here: points) more than once to
|
||||
/// differentiate between the start and end point of a route:
|
||||
/// ```
|
||||
/// var source = alias(points, 'source');
|
||||
/// var destination = alias(points, 'dest');
|
||||
///
|
||||
/// select(routes).join([
|
||||
/// innerJoin(source, routes.startPoint.equalsExp(source.id)),
|
||||
/// innerJoin(destination, routes.startPoint.equalsExp(destination.id)),
|
||||
/// ]);
|
||||
/// ```
|
||||
T alias<T extends Table, D extends DataClass>(
|
||||
TableInfo<T, D> table, String alias) {
|
||||
return table.createAlias(alias).asDslTable;
|
||||
}
|
||||
}
|
||||
|
||||
/// Mixin for a [DatabaseConnectionUser]. Provides an API to execute both
|
||||
/// high-level and custom queries and fetch their results.
|
||||
mixin QueryEngine on DatabaseConnectionUser {
|
||||
/// Whether this connection user is "top level", e.g. there is no parent
|
||||
/// connection user. We consider a [GeneratedDatabase] and a
|
||||
/// [DatabaseAccessor] to be top-level, while a [Transaction] or a
|
||||
/// [BeforeOpenEngine] aren't.
|
||||
/// [BeforeOpenRunner] aren't.
|
||||
///
|
||||
/// If any query method is called on a [topLevel] database user, we check if
|
||||
/// it could instead be delegated to a child executor. For instance, consider
|
||||
|
@ -156,17 +63,43 @@ mixin QueryEngine on DatabaseConnectionUser {
|
|||
TableInfo<Tbl, R> table) =>
|
||||
UpdateStatement(_resolvedEngine, table);
|
||||
|
||||
/// Starts a query on the given table. Queries can be limited with an limit
|
||||
/// or a where clause and can either return a current snapshot or a continuous
|
||||
/// stream of data
|
||||
/// Starts a query on the given table.
|
||||
///
|
||||
/// In moor, queries are commonly used as a builder by chaining calls on them
|
||||
/// using the `..` syntax from Dart. For instance, to load the 10 oldest users
|
||||
/// with an 'S' in their name, you could use:
|
||||
/// ```dart
|
||||
/// Future<List<User>> oldestUsers() {
|
||||
/// return (
|
||||
/// select(users)
|
||||
/// ..where((u) => u.name.like('%S%'))
|
||||
/// ..orderBy([(u) => OrderingTerm(
|
||||
/// expression: u.id,
|
||||
/// mode: OrderingMode.asc
|
||||
/// )])
|
||||
/// ..limit(10)
|
||||
/// ).get();
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// The [distinct] parameter (defaults to false) can be used to remove
|
||||
/// duplicate rows from the result set.
|
||||
///
|
||||
/// For more information on queries, see the
|
||||
/// [documentation](https://moor.simonbinder.eu/docs/getting-started/writing_queries/).
|
||||
@protected
|
||||
@visibleForTesting
|
||||
SimpleSelectStatement<T, R> select<T extends Table, R extends DataClass>(
|
||||
TableInfo<T, R> table) {
|
||||
return SimpleSelectStatement<T, R>(_resolvedEngine, table);
|
||||
TableInfo<T, R> table,
|
||||
{bool distinct = false}) {
|
||||
return SimpleSelectStatement<T, R>(_resolvedEngine, table,
|
||||
distinct: distinct);
|
||||
}
|
||||
|
||||
/// Starts a [DeleteStatement] that can be used to delete rows from a table.
|
||||
///
|
||||
/// See the [documentation](https://moor.simonbinder.eu/docs/getting-started/writing_queries/#updates-and-deletes)
|
||||
/// for more details and example on how delete statements work.
|
||||
@protected
|
||||
@visibleForTesting
|
||||
DeleteStatement<T, D> delete<T extends Table, D extends DataClass>(
|
||||
|
@ -288,8 +221,6 @@ mixin QueryEngine on DatabaseConnectionUser {
|
|||
/// might be different than that of the "global" database instance.
|
||||
/// 2. Nested transactions are not supported. Creating another transaction
|
||||
/// inside a transaction returns the parent transaction.
|
||||
@protected
|
||||
@visibleForTesting
|
||||
Future<T> transaction<T>(Future<T> Function() action) async {
|
||||
final resolved = _resolvedEngine;
|
||||
if (resolved is Transaction) {
|
||||
|
@ -314,7 +245,7 @@ mixin QueryEngine on DatabaseConnectionUser {
|
|||
rethrow;
|
||||
} finally {
|
||||
if (success) {
|
||||
// calling complete will also take care of committing the transaction
|
||||
// complete() will also take care of committing the transaction
|
||||
await transaction.complete();
|
||||
}
|
||||
}
|
||||
|
@ -322,6 +253,41 @@ mixin QueryEngine on DatabaseConnectionUser {
|
|||
});
|
||||
}
|
||||
|
||||
/// Runs statements inside a batch.
|
||||
///
|
||||
/// A batch can only run a subset of statements, and those statements must be
|
||||
/// called on the [Batch] instance. The statements aren't executed with a call
|
||||
/// to [Batch]. Instead, all generated queries are queued up and are then run
|
||||
/// and executed atomically in a transaction.
|
||||
/// If [batch] is called outside of a [transaction] call, it will implicitly
|
||||
/// start a transaction. Otherwise, the batch will re-use the transaction,
|
||||
/// and will have an effect when the transaction completes.
|
||||
/// Typically, running bulk updates (so a lot of similar statements) over a
|
||||
/// [Batch] is much faster than running them via the [GeneratedDatabase]
|
||||
/// directly.
|
||||
///
|
||||
/// An example that inserts users in a batch:
|
||||
/// ```dart
|
||||
/// await batch((b) {
|
||||
/// b.insertAll(
|
||||
/// todos,
|
||||
/// [
|
||||
/// TodosCompanion.insert(content: 'Use batches'),
|
||||
/// TodosCompanion.insert(content: 'Have fun'),
|
||||
/// ],
|
||||
/// );
|
||||
/// });
|
||||
/// ```
|
||||
@protected
|
||||
@visibleForTesting
|
||||
Future<void> batch(Function(Batch) runInBatch) {
|
||||
final engine = _resolvedEngine;
|
||||
|
||||
final batch = Batch._(engine, engine is! Transaction);
|
||||
runInBatch(batch);
|
||||
return batch._commit();
|
||||
}
|
||||
|
||||
/// Runs [calculation] in a forked [Zone] that has its [_resolvedEngine] set
|
||||
/// to the [engine].
|
||||
///
|
||||
|
@ -332,10 +298,13 @@ mixin QueryEngine on DatabaseConnectionUser {
|
|||
return runZoned(calculation, zoneValues: {_zoneRootUserKey: engine});
|
||||
}
|
||||
|
||||
/// Will be used by generated code to resolve inline Dart expressions in sql.
|
||||
/// Will be used by generated code to resolve inline Dart components in sql.
|
||||
@protected
|
||||
GenerationContext $write(Component component) {
|
||||
GenerationContext $write(Component component, {bool hasMultipleTables}) {
|
||||
final context = GenerationContext.fromDb(this);
|
||||
if (hasMultipleTables != null) {
|
||||
context.hasMultipleTables = hasMultipleTables;
|
||||
}
|
||||
|
||||
// we don't want ORDER BY clauses to write the ORDER BY tokens because those
|
||||
// are already declared in sql
|
||||
|
@ -348,87 +317,3 @@ mixin QueryEngine on DatabaseConnectionUser {
|
|||
return context;
|
||||
}
|
||||
}
|
||||
|
||||
/// A base class for all generated databases.
|
||||
abstract class GeneratedDatabase extends DatabaseConnectionUser
|
||||
with QueryEngine {
|
||||
@override
|
||||
final bool topLevel = true;
|
||||
|
||||
/// Specify the schema version of your database. Whenever you change or add
|
||||
/// tables, you should bump this field and provide a [migration] strategy.
|
||||
int get schemaVersion;
|
||||
|
||||
/// Defines the migration strategy that will determine how to deal with an
|
||||
/// increasing [schemaVersion]. The default value only supports creating the
|
||||
/// database by creating all tables known in this database. When you have
|
||||
/// changes in your schema, you'll need a custom migration strategy to create
|
||||
/// the new tables or change the columns.
|
||||
MigrationStrategy get migration => MigrationStrategy();
|
||||
MigrationStrategy _cachedMigration;
|
||||
MigrationStrategy get _resolvedMigration => _cachedMigration ??= migration;
|
||||
|
||||
/// A list of tables specified in this database.
|
||||
// todo: Replace all usages with allEntities.whereType()
|
||||
List<TableInfo> get allTables;
|
||||
|
||||
/// All entities (tables, views, triggers, indexes) that are declared in this
|
||||
/// database.
|
||||
List<DatabaseSchemaEntity> get allEntities => allTables;
|
||||
|
||||
/// Used by generated code
|
||||
GeneratedDatabase(SqlTypeSystem types, QueryExecutor executor,
|
||||
{StreamQueryStore streamStore})
|
||||
: super(types, executor, streamQueries: streamStore) {
|
||||
executor?.databaseInfo = this;
|
||||
}
|
||||
|
||||
/// Creates a [Migrator] with the provided query executor. Migrators generate
|
||||
/// sql statements to create or drop tables.
|
||||
///
|
||||
/// This api is mainly used internally in moor, for instance in
|
||||
/// [handleDatabaseCreation] and [handleDatabaseVersionChange]. However, it
|
||||
/// can also be used if you need to create tables manually and outside of a
|
||||
/// [MigrationStrategy]. For almost all use cases, overriding [migration]
|
||||
/// should suffice.
|
||||
@protected
|
||||
Migrator createMigrator([SqlExecutor executor]) {
|
||||
final actualExecutor = executor ?? customStatement;
|
||||
return Migrator(this, actualExecutor);
|
||||
}
|
||||
|
||||
/// Handles database creation by delegating the work to the [migration]
|
||||
/// strategy. This method should not be called by users.
|
||||
Future<void> handleDatabaseCreation({@required SqlExecutor executor}) {
|
||||
final migrator = createMigrator(executor);
|
||||
return _resolvedMigration.onCreate(migrator);
|
||||
}
|
||||
|
||||
/// Handles database updates by delegating the work to the [migration]
|
||||
/// strategy. This method should not be called by users.
|
||||
Future<void> handleDatabaseVersionChange(
|
||||
{@required SqlExecutor executor, int from, int to}) {
|
||||
final migrator = createMigrator(executor);
|
||||
return _resolvedMigration.onUpgrade(migrator, from, to);
|
||||
}
|
||||
|
||||
/// Handles the before opening callback as set in the [migration]. This method
|
||||
/// is used internally by database implementations and should not be called by
|
||||
/// users.
|
||||
Future<void> beforeOpenCallback(
|
||||
QueryExecutor executor, OpeningDetails details) async {
|
||||
final migration = _resolvedMigration;
|
||||
|
||||
if (migration.beforeOpen != null) {
|
||||
final engine = BeforeOpenEngine(this, executor);
|
||||
await _runEngineZoned(engine, () {
|
||||
return migration.beforeOpen(details);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Closes this database and releases associated resources.
|
||||
Future<void> close() async {
|
||||
await executor.close();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:moor/moor.dart';
|
||||
import 'package:moor/src/runtime/executor/stream_queries.dart';
|
||||
|
||||
part 'batch.dart';
|
||||
part 'connection.dart';
|
||||
part 'db_base.dart';
|
||||
part 'dao_base.dart';
|
||||
part 'query_engine.dart';
|
||||
|
||||
/// Defines additional runtime behavior for moor. Changing the fields of this
|
||||
/// class is rarely necessary.
|
||||
class MoorRuntimeOptions {
|
||||
/// Don't warn when a database class isn't used as singleton.
|
||||
bool dontWarnAboutMultipleDatabases = false;
|
||||
|
||||
/// The [ValueSerializer] that will be used by default in [DataClass.toJson].
|
||||
ValueSerializer defaultSerializer = const ValueSerializer.defaults();
|
||||
}
|
||||
|
||||
/// Stores the [MoorRuntimeOptions] describing global moor behavior across
|
||||
/// databases.
|
||||
///
|
||||
/// Note that is is adapting this behavior is rarely needed.
|
||||
MoorRuntimeOptions moorRuntimeOptions = MoorRuntimeOptions();
|
|
@ -5,6 +5,7 @@ import 'package:moor/moor.dart';
|
|||
|
||||
/// Common interface for objects which can be inserted or updated into a
|
||||
/// database.
|
||||
/// [D] is the associated data class.
|
||||
@optionalTypeArgs
|
||||
abstract class Insertable<D extends DataClass> {
|
||||
/// Converts this object into a companion that can be used for inserts. On
|
||||
|
@ -23,14 +24,15 @@ abstract class DataClass {
|
|||
|
||||
/// Converts this object into a representation that can be encoded with
|
||||
/// [json]. The [serializer] can be used to configure how individual values
|
||||
/// will be encoded.
|
||||
Map<String, dynamic> toJson(
|
||||
{ValueSerializer serializer = const ValueSerializer.defaults()});
|
||||
/// will be encoded. By default, [MoorRuntimeOptions.defaultSerializer] will
|
||||
/// be used. See [ValueSerializer.defaults] for details.
|
||||
Map<String, dynamic> toJson({ValueSerializer serializer});
|
||||
|
||||
/// Converts this object into a json representation. The [serializer] can be
|
||||
/// used to configure how individual values will be encoded.
|
||||
String toJsonString(
|
||||
{ValueSerializer serializer = const ValueSerializer.defaults()}) {
|
||||
/// used to configure how individual values will be encoded. By default,
|
||||
/// [MoorRuntimeOptions.defaultSerializer] will be used. See
|
||||
/// [ValueSerializer.defaults] for details.
|
||||
String toJsonString({ValueSerializer serializer}) {
|
||||
return json.encode(toJson(serializer: serializer));
|
||||
}
|
||||
|
||||
|
@ -44,6 +46,8 @@ abstract class DataClass {
|
|||
/// An update companion for a [DataClass] which is used to write data into a
|
||||
/// database using [InsertStatement.insert] or [UpdateStatement.write].
|
||||
///
|
||||
/// [D] is the associated data class for this companion.
|
||||
///
|
||||
/// See also:
|
||||
/// - the explanation in the changelog for 1.5
|
||||
/// - https://github.com/simolus3/moor/issues/25
|
||||
|
@ -83,8 +87,13 @@ abstract class ValueSerializer {
|
|||
/// Constant super-constructor to allow constant child classes.
|
||||
const ValueSerializer();
|
||||
|
||||
/// The default serializer encodes date times as a unix-timestamp in
|
||||
/// milliseconds.
|
||||
/// The builtin default serializer.
|
||||
///
|
||||
/// This serializer won't transform numbers or strings. Date times will be
|
||||
/// encoded as a unix-timestamp.
|
||||
///
|
||||
/// To override the default serializer moor uses, you can change the
|
||||
/// [MoorRuntimeOptions.defaultSerializer] field.
|
||||
const factory ValueSerializer.defaults() = _DefaultValueSerializer;
|
||||
|
||||
/// Converts the [value] to something that can be passed to
|
||||
|
@ -100,7 +109,7 @@ class _DefaultValueSerializer extends ValueSerializer {
|
|||
const _DefaultValueSerializer();
|
||||
|
||||
@override
|
||||
T fromJson<T>(json) {
|
||||
T fromJson<T>(dynamic json) {
|
||||
if (T == DateTime) {
|
||||
if (json == null) {
|
||||
return null;
|
||||
|
|
|
@ -34,6 +34,7 @@ class MoorWrappedException implements Exception {
|
|||
|
||||
@override
|
||||
String toString() {
|
||||
return '$cause at \n$trace\nMoor detected a possible cause for this: $message';
|
||||
return '$cause at \n$trace\n'
|
||||
'Moor detected a possible cause for this: $message';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
import 'package:meta/meta.dart';
|
||||
import 'package:moor/moor.dart';
|
||||
import 'package:moor/src/runtime/executor/stream_queries.dart';
|
||||
|
||||
/// Used internally by moor.
|
||||
class BeforeOpenEngine extends DatabaseConnectionUser with QueryEngine {
|
||||
/// Used internally by moor.
|
||||
BeforeOpenEngine(DatabaseConnectionUser other, QueryExecutor executor)
|
||||
: super.delegate(
|
||||
other,
|
||||
executor: executor,
|
||||
streamQueries: _IgnoreStreamQueries(),
|
||||
);
|
||||
|
||||
@override
|
||||
@alwaysThrows
|
||||
Future<T> transaction<T>(Function action) {
|
||||
throw UnsupportedError("Transactions can't be started inside beforeOpen");
|
||||
}
|
||||
}
|
||||
|
||||
class _IgnoreStreamQueries extends StreamQueryStore {
|
||||
@override
|
||||
Stream<T> registerStream<T>(QueryStreamFetcher<T> statement) {
|
||||
throw StateError('Streams cannot be created inside a transaction. See the '
|
||||
'documentation of GeneratedDatabase.transaction for details.');
|
||||
}
|
||||
|
||||
@override
|
||||
Future handleTableUpdates(Set<TableInfo> tables) {
|
||||
return Future.value(null);
|
||||
}
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:moor/src/runtime/components/component.dart';
|
||||
import 'package:moor/src/runtime/database.dart';
|
||||
import 'package:moor/backends.dart';
|
||||
import 'package:moor/moor.dart' show GeneratedDatabase;
|
||||
import 'package:moor/src/utils/hash.dart';
|
||||
|
||||
/// A query executor is responsible for executing statements on a database and
|
||||
|
@ -25,7 +25,7 @@ abstract class QueryExecutor {
|
|||
|
||||
/// Performs the async [fn] after this executor is ready, or directly if it's
|
||||
/// already ready.
|
||||
Future<T> doWhenOpened<T>(FutureOr<T> fn(QueryExecutor e)) {
|
||||
Future<T> doWhenOpened<T>(FutureOr<T> Function(QueryExecutor e) fn) {
|
||||
return ensureOpen().then((_) => fn(this));
|
||||
}
|
||||
|
||||
|
@ -87,7 +87,7 @@ class BatchedStatement {
|
|||
}
|
||||
|
||||
@override
|
||||
bool operator ==(other) {
|
||||
bool operator ==(dynamic other) {
|
||||
return identical(this, other) ||
|
||||
(other is BatchedStatement &&
|
||||
other.sql == sql &&
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import 'dart:async' show FutureOr;
|
||||
import 'dart:typed_data' show Uint8List;
|
||||
|
||||
import 'package:moor/moor.dart';
|
||||
import 'package:moor/src/runtime/components/component.dart';
|
||||
import 'package:moor/src/runtime/executor/helpers/results.dart';
|
||||
|
||||
/// An interface that supports sending database queries. Used as a backend for
|
||||
|
@ -47,8 +47,9 @@ abstract class DatabaseDelegate implements QueryDelegate {
|
|||
/// multiple times.
|
||||
///
|
||||
/// The [GeneratedDatabase] is the user-defined database annotated with
|
||||
/// [UseMoor]. It might be useful to read the [GeneratedDatabase.schemaVersion]
|
||||
/// if that information is required while opening the database.
|
||||
/// [UseMoor]. It might be useful to read the
|
||||
/// [GeneratedDatabase.schemaVersion] if that information is required while
|
||||
/// opening the database.
|
||||
Future<void> open([GeneratedDatabase db]);
|
||||
|
||||
/// Closes this database. When the future completes, all resources used
|
||||
|
@ -57,6 +58,12 @@ abstract class DatabaseDelegate implements QueryDelegate {
|
|||
// default no-op implementation
|
||||
}
|
||||
|
||||
/// Callback from moor after the database has been fully opened and all
|
||||
/// migrations ran.
|
||||
void notifyDatabaseOpened(OpeningDetails details) {
|
||||
// default no-op
|
||||
}
|
||||
|
||||
/// The [SqlDialect] understood by this database engine.
|
||||
SqlDialect get dialect => SqlDialect.sqlite;
|
||||
}
|
||||
|
@ -97,8 +104,8 @@ abstract class QueryDelegate {
|
|||
/// [BatchedStatement], which can be executed multiple times.
|
||||
Future<void> runBatched(List<BatchedStatement> statements) async {
|
||||
// default, inefficient implementation
|
||||
for (var stmt in statements) {
|
||||
for (var boundVars in stmt.variables) {
|
||||
for (final stmt in statements) {
|
||||
for (final boundVars in stmt.variables) {
|
||||
await runCustom(stmt.sql, boundVars);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:moor/moor.dart';
|
||||
import 'package:moor/src/runtime/components/component.dart';
|
||||
import 'package:pedantic/pedantic.dart';
|
||||
import 'package:synchronized/synchronized.dart';
|
||||
|
||||
|
@ -14,6 +13,10 @@ mixin _ExecutorWithQueryDelegate on QueryExecutor {
|
|||
bool get logStatements => false;
|
||||
final Lock _lock = Lock();
|
||||
|
||||
/// Used to provide better error messages when calling operations without
|
||||
/// calling [ensureOpen] before.
|
||||
bool _ensureOpenCalled = false;
|
||||
|
||||
Future<T> _synchronized<T>(FutureOr<T> Function() action) async {
|
||||
if (isSequential) {
|
||||
return await _lock.synchronized(action);
|
||||
|
@ -32,6 +35,7 @@ mixin _ExecutorWithQueryDelegate on QueryExecutor {
|
|||
@override
|
||||
Future<List<Map<String, dynamic>>> runSelect(
|
||||
String statement, List args) async {
|
||||
assert(_ensureOpenCalled);
|
||||
final result = await _synchronized(() {
|
||||
_log(statement, args);
|
||||
return impl.runSelect(statement, args);
|
||||
|
@ -41,6 +45,7 @@ mixin _ExecutorWithQueryDelegate on QueryExecutor {
|
|||
|
||||
@override
|
||||
Future<int> runUpdate(String statement, List args) {
|
||||
assert(_ensureOpenCalled);
|
||||
return _synchronized(() {
|
||||
_log(statement, args);
|
||||
return impl.runUpdate(statement, args);
|
||||
|
@ -49,6 +54,7 @@ mixin _ExecutorWithQueryDelegate on QueryExecutor {
|
|||
|
||||
@override
|
||||
Future<int> runDelete(String statement, List args) {
|
||||
assert(_ensureOpenCalled);
|
||||
return _synchronized(() {
|
||||
_log(statement, args);
|
||||
return impl.runUpdate(statement, args);
|
||||
|
@ -57,6 +63,7 @@ mixin _ExecutorWithQueryDelegate on QueryExecutor {
|
|||
|
||||
@override
|
||||
Future<int> runInsert(String statement, List args) {
|
||||
assert(_ensureOpenCalled);
|
||||
return _synchronized(() {
|
||||
_log(statement, args);
|
||||
return impl.runInsert(statement, args);
|
||||
|
@ -65,6 +72,7 @@ mixin _ExecutorWithQueryDelegate on QueryExecutor {
|
|||
|
||||
@override
|
||||
Future<void> runCustom(String statement, [List<dynamic> args]) {
|
||||
assert(_ensureOpenCalled);
|
||||
return _synchronized(() {
|
||||
final resolvedArgs = args ?? const [];
|
||||
_log(statement, resolvedArgs);
|
||||
|
@ -74,6 +82,7 @@ mixin _ExecutorWithQueryDelegate on QueryExecutor {
|
|||
|
||||
@override
|
||||
Future<void> runBatched(List<BatchedStatement> statements) {
|
||||
assert(_ensureOpenCalled);
|
||||
return _synchronized(() {
|
||||
if (logStatements) {
|
||||
print('Moor: Executing $statements in a batch');
|
||||
|
@ -114,6 +123,7 @@ class _TransactionExecutor extends TransactionExecutor
|
|||
|
||||
@override
|
||||
Future<bool> ensureOpen() async {
|
||||
_ensureOpenCalled = true;
|
||||
if (_openingCompleter != null) {
|
||||
return await _openingCompleter.future;
|
||||
}
|
||||
|
@ -190,34 +200,7 @@ class _TransactionExecutor extends TransactionExecutor
|
|||
}
|
||||
}
|
||||
|
||||
class _BeforeOpeningExecutor extends QueryExecutor
|
||||
with _ExecutorWithQueryDelegate {
|
||||
final DelegatedDatabase db;
|
||||
|
||||
@override
|
||||
QueryDelegate get impl => db.delegate;
|
||||
|
||||
@override
|
||||
bool get isSequential => db.isSequential;
|
||||
|
||||
@override
|
||||
bool get logStatements => db.logStatements;
|
||||
|
||||
_BeforeOpeningExecutor(this.db);
|
||||
|
||||
@override
|
||||
TransactionExecutor beginTransaction() {
|
||||
throw Exception(
|
||||
"Transactions can't be started in the before open callback");
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> ensureOpen() {
|
||||
return Future.value(true);
|
||||
}
|
||||
}
|
||||
|
||||
/// A database engine (implements [QueryExecutor]) that delegated the relevant
|
||||
/// A database engine (implements [QueryExecutor]) that delegates the relevant
|
||||
/// work to a [DatabaseDelegate].
|
||||
class DelegatedDatabase extends QueryExecutor with _ExecutorWithQueryDelegate {
|
||||
/// The [DatabaseDelegate] to send queries to.
|
||||
|
@ -245,12 +228,15 @@ class DelegatedDatabase extends QueryExecutor with _ExecutorWithQueryDelegate {
|
|||
|
||||
@override
|
||||
Future<bool> ensureOpen() {
|
||||
_ensureOpenCalled = true;
|
||||
return _openingLock.synchronized(() async {
|
||||
final alreadyOpen = await delegate.isOpen;
|
||||
if (alreadyOpen) {
|
||||
return true;
|
||||
}
|
||||
|
||||
assert(databaseInfo != null,
|
||||
'A databaseInfo needs to be set to use a QueryExeuctor');
|
||||
await delegate.open(databaseInfo);
|
||||
await _runMigrations();
|
||||
return true;
|
||||
|
@ -293,7 +279,9 @@ class DelegatedDatabase extends QueryExecutor with _ExecutorWithQueryDelegate {
|
|||
executor: runCustom, from: oldVersion, to: currentVersion);
|
||||
}
|
||||
|
||||
await _runBeforeOpen(OpeningDetails(oldVersion, currentVersion));
|
||||
final openingDetails = OpeningDetails(oldVersion, currentVersion);
|
||||
await _runBeforeOpen(openingDetails);
|
||||
delegate.notifyDatabaseOpened(openingDetails);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -305,3 +293,27 @@ class DelegatedDatabase extends QueryExecutor with _ExecutorWithQueryDelegate {
|
|||
return databaseInfo.beforeOpenCallback(_BeforeOpeningExecutor(this), d);
|
||||
}
|
||||
}
|
||||
|
||||
/// Inside a `beforeOpen` callback, all moor apis must be available. At the same
|
||||
/// time, the `beforeOpen` callback must complete before any query sent outside
|
||||
/// of a `beforeOpen` callback can run. We do this by introducing a special
|
||||
/// executor that delegates all work to the original executor, but without
|
||||
/// blocking on `ensureOpen`
|
||||
class _BeforeOpeningExecutor extends QueryExecutor
|
||||
with _ExecutorWithQueryDelegate {
|
||||
final DelegatedDatabase _base;
|
||||
|
||||
_BeforeOpeningExecutor(this._base);
|
||||
|
||||
@override
|
||||
TransactionExecutor beginTransaction() => _base.beginTransaction();
|
||||
|
||||
@override
|
||||
Future<bool> ensureOpen() {
|
||||
_ensureOpenCalled = true;
|
||||
return Future.value(true);
|
||||
}
|
||||
|
||||
@override
|
||||
QueryDelegate get impl => _base.impl;
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@ class StreamKey {
|
|||
}
|
||||
|
||||
@override
|
||||
bool operator ==(other) {
|
||||
bool operator ==(dynamic other) {
|
||||
return identical(this, other) ||
|
||||
(other is StreamKey &&
|
||||
other.sql == sql &&
|
||||
|
@ -61,8 +61,8 @@ class StreamKey {
|
|||
}
|
||||
}
|
||||
|
||||
/// Keeps track of active streams created from [SimpleSelectStatement]s and updates
|
||||
/// them when needed.
|
||||
/// Keeps track of active streams created from [SimpleSelectStatement]s and
|
||||
/// updates them when needed.
|
||||
class StreamQueryStore {
|
||||
final Map<StreamKey, QueryStream> _activeKeyStreams = {};
|
||||
final HashSet<StreamKey> _keysPendingRemoval = HashSet<StreamKey>();
|
||||
|
@ -103,7 +103,13 @@ class StreamQueryStore {
|
|||
/// Handles updates on a given table by re-executing all queries that read
|
||||
/// from that table.
|
||||
Future<void> handleTableUpdates(Set<TableInfo> tables) async {
|
||||
_updatedTableNames.add(tables.map((t) => t.actualTableName).toSet());
|
||||
handleTableUpdatesByName(tables.map((t) => t.actualTableName).toSet());
|
||||
}
|
||||
|
||||
/// Handles updates on tables by their name. All queries reading from any of
|
||||
/// the tables in [updatedTableNames] will fetch their data again.
|
||||
void handleTableUpdatesByName(Set<String> updatedTableNames) {
|
||||
_updatedTableNames.add(updatedTableNames);
|
||||
}
|
||||
|
||||
void markAsClosed(QueryStream stream) {
|
||||
|
@ -194,11 +200,18 @@ class QueryStream<T> {
|
|||
// Fetch data if it's needed, publish that data if it's possible.
|
||||
if (!_controller.hasListener) return;
|
||||
|
||||
final data = await _fetcher.fetchData();
|
||||
_lastData = data;
|
||||
T data;
|
||||
|
||||
if (!_controller.isClosed) {
|
||||
_controller.add(data);
|
||||
try {
|
||||
data = await _fetcher.fetchData();
|
||||
_lastData = data;
|
||||
if (!_controller.isClosed) {
|
||||
_controller.add(data);
|
||||
}
|
||||
} catch (e, s) {
|
||||
if (!_controller.isClosed) {
|
||||
_controller.addError(e, s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@ import 'package:moor/moor.dart';
|
|||
import 'package:moor/src/runtime/executor/stream_queries.dart';
|
||||
|
||||
/// Runs multiple statements transactionally.
|
||||
///
|
||||
/// Moor users should use [QueryEngine.transaction] to use this api.
|
||||
class Transaction extends DatabaseConnectionUser with QueryEngine {
|
||||
/// Constructs a transaction executor from the [other] user and the underlying
|
||||
/// [executor].
|
||||
|
@ -46,3 +48,14 @@ class _TransactionStreamStore extends StreamQueryStore {
|
|||
return parent.handleTableUpdates(affectedTables);
|
||||
}
|
||||
}
|
||||
|
||||
/// Special query engine to run the [MigrationStrategy.beforeOpen] callback.
|
||||
///
|
||||
/// To use this api, moor users should use the [MigrationStrategy.beforeOpen]
|
||||
/// parameter inside the [GeneratedDatabase.migration] getter.
|
||||
class BeforeOpenRunner extends DatabaseConnectionUser with QueryEngine {
|
||||
/// Creates a [BeforeOpenRunner] from the [database] and the special
|
||||
/// [executor] running the queries.
|
||||
BeforeOpenRunner(DatabaseConnectionUser database, QueryExecutor executor)
|
||||
: super.delegate(database, executor: executor);
|
||||
}
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
import 'package:moor/src/runtime/components/component.dart';
|
||||
import 'package:moor/src/runtime/expressions/expression.dart';
|
||||
import 'package:moor/src/types/sql_types.dart';
|
||||
|
||||
/// Returns an expression that is true iff both [a] and [b] are true.
|
||||
Expression<bool, BoolType> and(
|
||||
Expression<bool, BoolType> a, Expression<bool, BoolType> b) =>
|
||||
_AndExpression(a, b);
|
||||
|
||||
/// Returns an expression that is true iff [a], [b] or both are true.
|
||||
Expression<bool, BoolType> or(
|
||||
Expression<bool, BoolType> a, Expression<bool, BoolType> b) =>
|
||||
_OrExpression(a, b);
|
||||
|
||||
/// Returns an expression that is true iff [a] is not true.
|
||||
Expression<bool, BoolType> not(Expression<bool, BoolType> a) =>
|
||||
_NotExpression(a);
|
||||
|
||||
class _AndExpression extends InfixOperator<bool, BoolType> {
|
||||
@override
|
||||
Expression<bool, BoolType> left, right;
|
||||
|
||||
@override
|
||||
final String operator = 'AND';
|
||||
|
||||
_AndExpression(this.left, this.right);
|
||||
}
|
||||
|
||||
class _OrExpression extends InfixOperator<bool, BoolType> {
|
||||
@override
|
||||
Expression<bool, BoolType> left, right;
|
||||
|
||||
@override
|
||||
final String operator = 'OR';
|
||||
|
||||
_OrExpression(this.left, this.right);
|
||||
}
|
||||
|
||||
class _NotExpression extends Expression<bool, BoolType> {
|
||||
Expression<bool, BoolType> inner;
|
||||
|
||||
_NotExpression(this.inner);
|
||||
|
||||
@override
|
||||
void writeInto(GenerationContext context) {
|
||||
context.buffer.write('NOT ');
|
||||
inner.writeInto(context);
|
||||
}
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
import 'package:moor/moor.dart';
|
||||
import 'package:moor/src/runtime/components/component.dart';
|
||||
import 'package:moor/src/runtime/expressions/custom.dart';
|
||||
import 'package:moor/src/runtime/expressions/expression.dart';
|
||||
|
||||
/// Extracts the (UTC) year from the given expression that resolves
|
||||
/// to a datetime.
|
||||
Expression<int, IntType> year(Expression<DateTime, DateTimeType> date) =>
|
||||
_StrftimeSingleFieldExpression('%Y', date);
|
||||
|
||||
/// Extracts the (UTC) month from the given expression that resolves
|
||||
/// to a datetime.
|
||||
Expression<int, IntType> month(Expression<DateTime, DateTimeType> date) =>
|
||||
_StrftimeSingleFieldExpression('%m', date);
|
||||
|
||||
/// Extracts the (UTC) day from the given expression that resolves
|
||||
/// to a datetime.
|
||||
Expression<int, IntType> day(Expression<DateTime, DateTimeType> date) =>
|
||||
_StrftimeSingleFieldExpression('%d', date);
|
||||
|
||||
/// Extracts the (UTC) hour from the given expression that resolves
|
||||
/// to a datetime.
|
||||
Expression<int, IntType> hour(Expression<DateTime, DateTimeType> date) =>
|
||||
_StrftimeSingleFieldExpression('%H', date);
|
||||
|
||||
/// Extracts the (UTC) minute from the given expression that resolves
|
||||
/// to a datetime.
|
||||
Expression<int, IntType> minute(Expression<DateTime, DateTimeType> date) =>
|
||||
_StrftimeSingleFieldExpression('%M', date);
|
||||
|
||||
/// Extracts the (UTC) second from the given expression that resolves
|
||||
/// to a datetime.
|
||||
Expression<int, IntType> second(Expression<DateTime, DateTimeType> date) =>
|
||||
_StrftimeSingleFieldExpression('%S', date);
|
||||
|
||||
/// A sql expression that evaluates to the current date represented as a unix
|
||||
/// timestamp. The hour, minute and second fields will be set to 0.
|
||||
const DateTimeExpression currentDate =
|
||||
_CustomDateTimeExpression("strftime('%s', CURRENT_DATE)");
|
||||
|
||||
/// A sql expression that evaluates to the current date and time, similar to
|
||||
/// [DateTime.now]. Timestamps are stored with a second accuracy.
|
||||
const DateTimeExpression currentDateAndTime =
|
||||
_CustomDateTimeExpression("strftime('%s', CURRENT_TIMESTAMP)");
|
||||
|
||||
class _CustomDateTimeExpression extends CustomExpression<DateTime, DateTimeType>
|
||||
with ComparableExpr
|
||||
implements DateTimeExpression {
|
||||
const _CustomDateTimeExpression(String content) : super(content);
|
||||
}
|
||||
|
||||
/// Expression that extracts components out of a date time by using the builtin
|
||||
/// sqlite function "strftime" and casting the result to an integer.
|
||||
class _StrftimeSingleFieldExpression extends Expression<int, IntType> {
|
||||
final String format;
|
||||
final Expression<DateTime, DateTimeType> date;
|
||||
|
||||
_StrftimeSingleFieldExpression(this.format, this.date);
|
||||
|
||||
@override
|
||||
void writeInto(GenerationContext context) {
|
||||
context.buffer.write('CAST(strftime("$format", ');
|
||||
date.writeInto(context);
|
||||
context.buffer.write(', "unixepoch") AS INTEGER)');
|
||||
}
|
||||
}
|
|
@ -1,116 +0,0 @@
|
|||
import 'package:meta/meta.dart';
|
||||
import 'package:moor/moor.dart';
|
||||
import 'package:moor/src/runtime/components/component.dart';
|
||||
import 'package:moor/src/types/sql_types.dart';
|
||||
|
||||
/// Any sql expression that evaluates to some generic value. This does not
|
||||
/// include queries (which might evaluate to multiple values) but individual
|
||||
/// columns, functions and operators.
|
||||
abstract class Expression<D, T extends SqlType<D>> implements Component {
|
||||
/// Constant constructor so that subclasses can be constant.
|
||||
const Expression();
|
||||
|
||||
/// Whether this expression is a literal. Some use-sites need to put
|
||||
/// parentheses around non-literals.
|
||||
bool get isLiteral => false;
|
||||
|
||||
/// Whether this expression is equal to the given expression.
|
||||
Expression<bool, BoolType> equalsExp(Expression<D, T> compare) =>
|
||||
Comparison.equal(this, compare);
|
||||
|
||||
/// Whether this column is equal to the given value, which must have a fitting
|
||||
/// type. The [compare] value will be written
|
||||
/// as a variable using prepared statements, so there is no risk of
|
||||
/// an SQL-injection.
|
||||
Expression<bool, BoolType> equals(D compare) =>
|
||||
Comparison.equal(this, Variable<D, T>(compare));
|
||||
}
|
||||
|
||||
/// An expression that looks like "$a operator $b", where $a and $b itself
|
||||
/// are expressions and the operator is any string.
|
||||
abstract class InfixOperator<D, T extends SqlType<D>> extends Expression<D, T> {
|
||||
/// The left-hand side of this expression
|
||||
Expression get left;
|
||||
|
||||
/// The right-hand side of this expresion
|
||||
Expression get right;
|
||||
|
||||
/// The sql operator to write
|
||||
String get operator;
|
||||
|
||||
/// Whether we should put parentheses around the [left] and [right]
|
||||
/// expressions.
|
||||
@visibleForOverriding
|
||||
bool get placeBrackets => true;
|
||||
|
||||
@override
|
||||
void writeInto(GenerationContext context) {
|
||||
_placeBracketIfNeeded(context, true);
|
||||
|
||||
left.writeInto(context);
|
||||
|
||||
_placeBracketIfNeeded(context, false);
|
||||
context.writeWhitespace();
|
||||
context.buffer.write(operator);
|
||||
context.writeWhitespace();
|
||||
_placeBracketIfNeeded(context, true);
|
||||
|
||||
right.writeInto(context);
|
||||
|
||||
_placeBracketIfNeeded(context, false);
|
||||
}
|
||||
|
||||
void _placeBracketIfNeeded(GenerationContext context, bool open) {
|
||||
if (placeBrackets) context.buffer.write(open ? '(' : ')');
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines the possible comparison operators that can appear in a [Comparison].
|
||||
enum ComparisonOperator {
|
||||
/// '<' in sql
|
||||
less,
|
||||
|
||||
/// '<=' in sql
|
||||
lessOrEqual,
|
||||
|
||||
/// '=' in sql
|
||||
equal,
|
||||
|
||||
/// '>=' in sql
|
||||
moreOrEqual,
|
||||
|
||||
/// '>' in sql
|
||||
more
|
||||
}
|
||||
|
||||
/// An expression that compares two child expressions.
|
||||
class Comparison extends InfixOperator<bool, BoolType> {
|
||||
static const Map<ComparisonOperator, String> _operatorNames = {
|
||||
ComparisonOperator.less: '<',
|
||||
ComparisonOperator.lessOrEqual: '<=',
|
||||
ComparisonOperator.equal: '=',
|
||||
ComparisonOperator.moreOrEqual: '>=',
|
||||
ComparisonOperator.more: '>'
|
||||
};
|
||||
|
||||
@override
|
||||
final Expression left;
|
||||
@override
|
||||
final Expression right;
|
||||
|
||||
/// The operator to use for this comparison
|
||||
final ComparisonOperator op;
|
||||
|
||||
@override
|
||||
final bool placeBrackets = false;
|
||||
|
||||
@override
|
||||
String get operator => _operatorNames[op];
|
||||
|
||||
/// Constructs a comparison from the [left] and [right] expressions to compare
|
||||
/// and the [ComparisonOperator] [op].
|
||||
Comparison(this.left, this.op, this.right);
|
||||
|
||||
/// Like [Comparison(left, op, right)], but uses [ComparisonOperator.equal].
|
||||
Comparison.equal(this.left, this.right) : op = ComparisonOperator.equal;
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
import 'package:moor/src/runtime/components/component.dart';
|
||||
import 'package:moor/src/runtime/expressions/expression.dart';
|
||||
import 'package:moor/src/types/sql_types.dart';
|
||||
|
||||
/// A `text LIKE pattern` expression that will be true if the first expression
|
||||
/// matches the pattern given by the second expression.
|
||||
class LikeOperator extends Expression<bool, BoolType> {
|
||||
/// The target expression that will be tested
|
||||
final Expression<String, StringType> target;
|
||||
|
||||
/// The regex-like expression to test the [target] against.
|
||||
final Expression<String, StringType> regex;
|
||||
|
||||
/// Perform a like operator with the target and the regex.
|
||||
LikeOperator(this.target, this.regex);
|
||||
|
||||
@override
|
||||
void writeInto(GenerationContext context) {
|
||||
target.writeInto(context);
|
||||
context.buffer.write(' LIKE ');
|
||||
regex.writeInto(context);
|
||||
}
|
||||
}
|
||||
|
||||
/// Builtin collating functions from sqlite.
|
||||
///
|
||||
/// See also:
|
||||
/// - https://www.sqlite.org/datatype3.html#collation
|
||||
enum Collate {
|
||||
/// Instruct sqlite to compare string data using memcmp(), regardless of text
|
||||
/// encoding.
|
||||
binary,
|
||||
|
||||
/// The same as [Collate.binary], except the 26 upper case characters of ASCII
|
||||
/// are folded to their lower case equivalents before the comparison is
|
||||
/// performed. Note that only ASCII characters are case folded. SQLite does
|
||||
/// not attempt to do full UTF case folding due to the size of the tables
|
||||
/// required.
|
||||
noCase,
|
||||
|
||||
/// The same as [Collate.binary], except that trailing space characters are
|
||||
/// ignored.
|
||||
rTrim,
|
||||
}
|
||||
|
||||
/// A `text COLLATE collate` expression in sqlite.
|
||||
class CollateOperator extends Expression<String, StringType> {
|
||||
/// The expression on which the collate function will be run
|
||||
final Expression inner;
|
||||
|
||||
/// The [Collate] to use.
|
||||
final Collate collate;
|
||||
|
||||
/// Constructs a collate expression on the [inner] expression and the
|
||||
/// [Collate].
|
||||
CollateOperator(this.inner, this.collate);
|
||||
|
||||
@override
|
||||
void writeInto(GenerationContext context) {
|
||||
inner.writeInto(context);
|
||||
context.buffer..write(' COLLATE ')..write(_operatorNames[collate]);
|
||||
}
|
||||
|
||||
static const Map<Collate, String> _operatorNames = {
|
||||
Collate.binary: 'BINARY',
|
||||
Collate.noCase: 'NOCASE',
|
||||
Collate.rTrim: 'RTRIM',
|
||||
};
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
export 'bools.dart' show and, or, not;
|
||||
export 'custom.dart';
|
||||
export 'datetimes.dart';
|
||||
export 'expression.dart' show Expression;
|
||||
export 'in.dart';
|
||||
export 'null_check.dart';
|
||||
export 'text.dart' show Collate;
|
||||
export 'variables.dart';
|
|
@ -0,0 +1,183 @@
|
|||
part of 'moor_isolate.dart';
|
||||
|
||||
class _MoorClient {
|
||||
final IsolateCommunication _channel;
|
||||
final SqlTypeSystem typeSystem;
|
||||
_IsolateStreamQueryStore _streamStore;
|
||||
|
||||
DatabaseConnection _connection;
|
||||
|
||||
GeneratedDatabase get connectedDb => _connection.executor.databaseInfo;
|
||||
|
||||
SqlExecutor get executor => _connection.executor.runCustom;
|
||||
|
||||
_MoorClient(this._channel, this.typeSystem) {
|
||||
_streamStore = _IsolateStreamQueryStore(this);
|
||||
|
||||
_connection = DatabaseConnection(
|
||||
typeSystem,
|
||||
_IsolateQueryExecutor(this),
|
||||
_streamStore,
|
||||
);
|
||||
_channel.setRequestHandler(_handleRequest);
|
||||
}
|
||||
|
||||
static Future<_MoorClient> connect(
|
||||
MoorIsolate isolate, bool isolateDebugLog) async {
|
||||
final connection = await IsolateCommunication.connectAsClient(
|
||||
isolate._server, isolateDebugLog);
|
||||
|
||||
final typeSystem =
|
||||
await connection.request<SqlTypeSystem>(_NoArgsRequest.getTypeSystem);
|
||||
return _MoorClient(connection, typeSystem);
|
||||
}
|
||||
|
||||
dynamic _handleRequest(Request request) {
|
||||
final payload = request.payload;
|
||||
|
||||
if (payload is _NoArgsRequest) {
|
||||
switch (payload) {
|
||||
case _NoArgsRequest.runOnCreate:
|
||||
return connectedDb.handleDatabaseCreation(executor: executor);
|
||||
default:
|
||||
throw UnsupportedError('This operation must be run on the server');
|
||||
}
|
||||
} else if (payload is _RunOnUpgrade) {
|
||||
return connectedDb.handleDatabaseVersionChange(
|
||||
executor: executor,
|
||||
from: payload.versionBefore,
|
||||
to: payload.versionNow,
|
||||
);
|
||||
} else if (payload is _RunBeforeOpen) {
|
||||
return connectedDb.beforeOpenCallback(
|
||||
_connection.executor, payload.details);
|
||||
} else if (payload is _NotifyTablesUpdated) {
|
||||
_streamStore.handleTableUpdatesByName(payload.updatedTables.toSet());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _BaseExecutor extends QueryExecutor {
|
||||
final _MoorClient client;
|
||||
int _transactionId;
|
||||
|
||||
_BaseExecutor(this.client);
|
||||
|
||||
@override
|
||||
Future<void> runBatched(List<BatchedStatement> statements) {
|
||||
return client._channel.request(_ExecuteBatchedStatement(statements));
|
||||
}
|
||||
|
||||
Future<T> _runRequest<T>(_StatementMethod method, String sql, List args) {
|
||||
return client._channel.request<T>(_ExecuteQuery(method, sql, args));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> runCustom(String statement, [List args]) {
|
||||
return _runRequest(_StatementMethod.custom, statement, args);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> runDelete(String statement, List args) {
|
||||
return _runRequest(_StatementMethod.deleteOrUpdate, statement, args);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> runUpdate(String statement, List args) {
|
||||
return _runRequest(_StatementMethod.deleteOrUpdate, statement, args);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> runInsert(String statement, List args) {
|
||||
return _runRequest(_StatementMethod.insert, statement, args);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Map<String, dynamic>>> runSelect(String statement, List args) {
|
||||
return _runRequest(_StatementMethod.select, statement, args);
|
||||
}
|
||||
}
|
||||
|
||||
class _IsolateQueryExecutor extends _BaseExecutor {
|
||||
_IsolateQueryExecutor(_MoorClient client) : super(client);
|
||||
|
||||
@override
|
||||
set databaseInfo(GeneratedDatabase db) {
|
||||
super.databaseInfo = db;
|
||||
client._channel.request(_SetSchemaVersion(db.schemaVersion));
|
||||
}
|
||||
|
||||
@override
|
||||
TransactionExecutor beginTransaction() {
|
||||
return _TransactionIsolateExecutor(client);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> ensureOpen() {
|
||||
return client._channel.request<bool>(_NoArgsRequest.ensureOpen);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
client._channel.close();
|
||||
return Future.value();
|
||||
}
|
||||
}
|
||||
|
||||
class _TransactionIsolateExecutor extends _BaseExecutor
|
||||
implements TransactionExecutor {
|
||||
_TransactionIsolateExecutor(_MoorClient client) : super(client);
|
||||
|
||||
bool _pendingOpen = false;
|
||||
|
||||
// nested transactions aren't supported
|
||||
@override
|
||||
TransactionExecutor beginTransaction() => null;
|
||||
|
||||
@override
|
||||
Future<bool> ensureOpen() {
|
||||
if (_transactionId == null && !_pendingOpen) {
|
||||
_pendingOpen = true;
|
||||
return _openAtServer().then((_) => true);
|
||||
}
|
||||
return Future.value(true);
|
||||
}
|
||||
|
||||
Future _openAtServer() async {
|
||||
_transactionId =
|
||||
await client._channel.request(_NoArgsRequest.startTransaction) as int;
|
||||
_pendingOpen = false;
|
||||
}
|
||||
|
||||
Future<void> _sendAction(_TransactionControl action) {
|
||||
return client._channel
|
||||
.request(_RunTransactionAction(action, _transactionId));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> rollback() {
|
||||
return _sendAction(_TransactionControl.rollback);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> send() {
|
||||
return _sendAction(_TransactionControl.commit);
|
||||
}
|
||||
}
|
||||
|
||||
class _IsolateStreamQueryStore extends StreamQueryStore {
|
||||
final _MoorClient client;
|
||||
|
||||
_IsolateStreamQueryStore(this.client);
|
||||
|
||||
@override
|
||||
Future<void> handleTableUpdates(Set<TableInfo> tables) {
|
||||
// we're not calling super.handleTableUpdates because the server will send
|
||||
// a notification of those tables to all clients, including the one who sent
|
||||
// this. When we get that reply, we update the tables.
|
||||
// Note that we're not running into an infinite feedback loop because the
|
||||
// client will call handleTableUpdatesByName. That's kind of a hack.
|
||||
return client._channel.request(
|
||||
_NotifyTablesUpdated(tables.map((t) => t.actualTableName).toList()));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,283 @@
|
|||
import 'dart:async';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:pedantic/pedantic.dart';
|
||||
|
||||
/// An isolate communication setup where there's a single "server" isolate that
|
||||
/// communicates with a varying amount of "client" isolates.
|
||||
///
|
||||
/// Each communication is bi-directional, meaning that both the server and the
|
||||
/// client can send requests to each other and expect responses for that.
|
||||
class IsolateCommunication {
|
||||
/// The [SendPort] used to send messages to the peer.
|
||||
final SendPort sendPort;
|
||||
|
||||
/// The input stream of this channel. This could be a [ReceivePort].
|
||||
final Stream<dynamic> input;
|
||||
StreamSubscription _inputSubscription;
|
||||
|
||||
// note that there are two IsolateCommunication instances in each connection,
|
||||
// and each of them has an independent _currentRequestId field!
|
||||
int _currentRequestId = 0;
|
||||
final Completer<void> _closeCompleter = Completer();
|
||||
final Map<int, Completer> _pendingRequests = {};
|
||||
final StreamController<Request> _incomingRequests = StreamController();
|
||||
|
||||
final bool _debugLog;
|
||||
|
||||
IsolateCommunication._(this.sendPort, this.input, [this._debugLog = false]) {
|
||||
_inputSubscription = input.listen(_handleMessage);
|
||||
}
|
||||
|
||||
/// Returns a future that resolves when this communication channel was closed,
|
||||
/// either via a call to [close] from this isolate or from the other isolate.
|
||||
Future<void> get closed => _closeCompleter.future;
|
||||
|
||||
/// A stream of requests coming from the other peer.
|
||||
Stream<Request> get incomingRequests => _incomingRequests.stream;
|
||||
|
||||
/// Establishes an [IsolateCommunication] by connecting to the [Server] which
|
||||
/// emitted the [key].
|
||||
static Future<IsolateCommunication> connectAsClient(ServerKey key,
|
||||
[bool debugLog = false]) async {
|
||||
final clientReceive = ReceivePort();
|
||||
final stream = clientReceive.asBroadcastStream();
|
||||
|
||||
key.openConnectionPort
|
||||
.send(_ClientConnectionRequest(clientReceive.sendPort));
|
||||
|
||||
final response = await stream.first as _ServerConnectionResponse;
|
||||
|
||||
final communication =
|
||||
IsolateCommunication._(response.sendPort, stream, debugLog);
|
||||
|
||||
unawaited(communication.closed.then((_) => clientReceive.close()));
|
||||
|
||||
return communication;
|
||||
}
|
||||
|
||||
/// Closes the connection to the server.
|
||||
void close() {
|
||||
_send(_ConnectionClose());
|
||||
_closeLocally();
|
||||
}
|
||||
|
||||
void _closeLocally() {
|
||||
_inputSubscription?.cancel();
|
||||
_closeCompleter.complete();
|
||||
|
||||
for (final pending in _pendingRequests.values) {
|
||||
pending.completeError(StateError('connection closed'));
|
||||
}
|
||||
_pendingRequests.clear();
|
||||
}
|
||||
|
||||
void _handleMessage(dynamic msg) {
|
||||
if (_debugLog) {
|
||||
print('[IN]: $msg');
|
||||
}
|
||||
|
||||
if (msg is _ConnectionClose) {
|
||||
_closeLocally();
|
||||
} else if (msg is _Response) {
|
||||
final completer = _pendingRequests[msg.requestId];
|
||||
|
||||
if (completer != null) {
|
||||
if (msg is _ErrorResponse) {
|
||||
final trace = msg.stackTrace != null
|
||||
? StackTrace.fromString(msg.stackTrace)
|
||||
: null;
|
||||
|
||||
completer.completeError(msg.error, trace);
|
||||
} else {
|
||||
completer.complete(msg.response);
|
||||
}
|
||||
|
||||
_pendingRequests.remove(msg.requestId);
|
||||
}
|
||||
} else if (msg is Request) {
|
||||
_incomingRequests.add(msg);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends a request and waits for the peer to reply with a value that is
|
||||
/// assumed to be of type [T].
|
||||
Future<T> request<T>(dynamic request) {
|
||||
final id = _currentRequestId++;
|
||||
final completer = Completer<T>();
|
||||
|
||||
_pendingRequests[id] = completer;
|
||||
_send(Request._(id, request));
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
void _send(dynamic msg) {
|
||||
if (_debugLog) {
|
||||
print('[OUT]: $msg');
|
||||
}
|
||||
sendPort.send(msg);
|
||||
}
|
||||
|
||||
/// Sends a response for a handled [Request].
|
||||
void respond(Request request, dynamic response) {
|
||||
_send(_Response(request.id, response));
|
||||
}
|
||||
|
||||
/// Sends an erroneous response for a [Request].
|
||||
void respondError(Request request, dynamic error, [StackTrace trace]) {
|
||||
_send(_ErrorResponse(request.id, error, trace.toString()));
|
||||
}
|
||||
|
||||
/// Utility that listens to [incomingRequests] and invokes the [handler] on
|
||||
/// each request, sending the result back to the originating client. If
|
||||
/// [handler] throws, the error will be re-directed to the client. If
|
||||
/// [handler] returns a [Future], it will be awaited.
|
||||
void setRequestHandler(dynamic Function(Request) handler) {
|
||||
incomingRequests.listen((request) {
|
||||
try {
|
||||
final result = handler(request);
|
||||
|
||||
if (result is Future) {
|
||||
result.then((value) => respond(request, value));
|
||||
} else {
|
||||
respond(request, result);
|
||||
}
|
||||
} catch (e, s) {
|
||||
respondError(request, e, s);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// A key generated by the server than can be sent across isolates. A client can
|
||||
/// connect to a server by its key.
|
||||
class ServerKey {
|
||||
/// The [SendPort] used by clients to establish an [IsolateCommunication] with
|
||||
/// this server.
|
||||
final SendPort openConnectionPort;
|
||||
|
||||
ServerKey._(this.openConnectionPort);
|
||||
}
|
||||
|
||||
/// Contains logic to implement the server isolate as described in
|
||||
/// [IsolateCommunication]. Note that an instance of this class should not be
|
||||
/// sent across isolates, use the []
|
||||
class Server {
|
||||
final ReceivePort _openConnectionPort = ReceivePort();
|
||||
final StreamController<IsolateCommunication> _opened = StreamController();
|
||||
|
||||
ServerKey _key;
|
||||
|
||||
/// Returns all communication channels currently opened to this server.
|
||||
final List<IsolateCommunication> currentChannels = [];
|
||||
|
||||
/// An identifier of this [Server] allowing a client to
|
||||
/// [IsolateCommunication.connectAsClient].
|
||||
ServerKey get key => _key;
|
||||
|
||||
/// A stream of established [IsolateCommunication] channels after they were
|
||||
/// opened by the client. This is not a broadcast stream.
|
||||
Stream<IsolateCommunication> get openedConnections => _opened.stream;
|
||||
|
||||
/// Opens a server in the current isolate.
|
||||
Server() {
|
||||
_key = ServerKey._(_openConnectionPort.sendPort);
|
||||
|
||||
_openConnectionPort.listen(_handleMessageOnConnectionPort);
|
||||
}
|
||||
|
||||
/// Closes this server instance and disposes associated resources.
|
||||
void close() {
|
||||
_openConnectionPort.close();
|
||||
_opened.close();
|
||||
|
||||
for (final connected in currentChannels) {
|
||||
connected.close();
|
||||
}
|
||||
}
|
||||
|
||||
void _handleMessageOnConnectionPort(dynamic message) {
|
||||
if (message is _ClientConnectionRequest) {
|
||||
final receiveFromClient = ReceivePort();
|
||||
final communication =
|
||||
IsolateCommunication._(message.sendPort, receiveFromClient);
|
||||
|
||||
currentChannels.add(communication);
|
||||
_opened.add(communication);
|
||||
|
||||
final response = _ServerConnectionResponse(receiveFromClient.sendPort);
|
||||
message.sendPort.send(response);
|
||||
|
||||
communication.closed.whenComplete(() {
|
||||
currentChannels.remove(communication);
|
||||
receiveFromClient.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sent from a client to a [ServerKey.openConnectionPort] in order to
|
||||
/// establish a connection.
|
||||
class _ClientConnectionRequest {
|
||||
/// The [SendPort] to use by a [Server] to send messages to the client
|
||||
/// sending the connection request.
|
||||
final SendPort sendPort;
|
||||
|
||||
_ClientConnectionRequest(this.sendPort);
|
||||
}
|
||||
|
||||
/// Reply from a [Server] to a [_ClientConnectionRequest] to indicate that the
|
||||
/// connection has been established.
|
||||
class _ServerConnectionResponse {
|
||||
/// The [SendPort] used by the client to send further messages to the
|
||||
/// [Server].
|
||||
final SendPort sendPort;
|
||||
|
||||
_ServerConnectionResponse(this.sendPort);
|
||||
}
|
||||
|
||||
/// Sent from any peer to close the connection.
|
||||
class _ConnectionClose {}
|
||||
|
||||
/// A request sent over an isolate connection. It is expected that the other
|
||||
/// peer eventually answers with a matching response.
|
||||
class Request {
|
||||
/// The id of this request, generated by the sender.
|
||||
final int id;
|
||||
|
||||
/// The payload associated with this request
|
||||
final dynamic payload;
|
||||
|
||||
Request._(this.id, this.payload);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'request (id = $id): $payload';
|
||||
}
|
||||
}
|
||||
|
||||
class _Response {
|
||||
final int requestId;
|
||||
final dynamic response;
|
||||
|
||||
_Response(this.requestId, this.response);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'response (id = $requestId): $response';
|
||||
}
|
||||
}
|
||||
|
||||
class _ErrorResponse extends _Response {
|
||||
final String stackTrace;
|
||||
|
||||
dynamic get error => response;
|
||||
|
||||
_ErrorResponse(int requestId, dynamic error, [this.stackTrace])
|
||||
: super(requestId, error);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'error response (id = $requestId): $error at $stackTrace';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
import 'dart:async';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:moor/moor.dart';
|
||||
import 'package:moor/src/runtime/executor/stream_queries.dart';
|
||||
import 'package:pedantic/pedantic.dart';
|
||||
import 'communication.dart';
|
||||
|
||||
part 'client.dart';
|
||||
part 'protocol.dart';
|
||||
part 'server.dart';
|
||||
|
||||
/// Signature of a function that opens a database connection.
|
||||
typedef DatabaseOpener = DatabaseConnection Function();
|
||||
|
||||
/// Defines utilities to run moor in a background isolate. In the operation mode
|
||||
/// created by these utilities, there's a single background isolate doing all
|
||||
/// the work. Any other isolate can use the [connect] method to obtain an
|
||||
/// instance of a [GeneratedDatabase] class that will delegate its work onto a
|
||||
/// background isolate. Auto-updating queries, and transactions work across
|
||||
/// isolates, and the user facing api is exactly the same.
|
||||
///
|
||||
/// Please note that, while running moor in a background isolate can reduce
|
||||
/// latency in foreground isolates (thus reducing UI lags), the overall
|
||||
/// performance is going to be much worse as data has to be serialized and
|
||||
/// deserialized to be sent over isolates.
|
||||
/// Also, be aware that this api is not available on the web.
|
||||
///
|
||||
/// See also:
|
||||
/// - [Isolate], for general information on multi threading in Dart.
|
||||
/// - The [detailed documentation](https://moor.simonbinder.eu/docs/advanced-features/isolates),
|
||||
/// which provides example codes on how to use this api.
|
||||
class MoorIsolate {
|
||||
/// Identifier for the server isolate that we can connect to.
|
||||
final ServerKey _server;
|
||||
|
||||
MoorIsolate._(this._server);
|
||||
|
||||
/// Connects to this [MoorIsolate] from another isolate. All operations on the
|
||||
/// returned [DatabaseConnection] will be executed on a background isolate.
|
||||
/// Setting the [isolateDebugLog] is only helpful when debugging moor itself.
|
||||
Future<DatabaseConnection> connect({bool isolateDebugLog = false}) async {
|
||||
final client = await _MoorClient.connect(this, isolateDebugLog);
|
||||
return client._connection;
|
||||
}
|
||||
|
||||
/// Stops the background isolate and disconnects all [DatabaseConnection]s
|
||||
/// created.
|
||||
/// If you only want to disconnect a database connection created via
|
||||
/// [connect], use [GeneratedDatabase.close] instead.
|
||||
Future<void> shutdownAll() async {
|
||||
final connection = await IsolateCommunication.connectAsClient(_server);
|
||||
unawaited(connection.request(_NoArgsRequest.terminateAll).then((_) {},
|
||||
onError: (_) {
|
||||
// the background isolate is closed before it gets a chance to reply
|
||||
// to the terminateAll request. Ignore the error
|
||||
}));
|
||||
|
||||
await connection.closed;
|
||||
}
|
||||
|
||||
/// Creates a new [MoorIsolate] on a background thread.
|
||||
///
|
||||
/// The [opener] function will be used to open the [DatabaseConnection] used
|
||||
/// by the isolate. Most implementations are likely to use
|
||||
/// [DatabaseConnection.fromExecutor] instead of providing stream queries and
|
||||
/// the type system manually.
|
||||
///
|
||||
/// Because [opener] will be called on another isolate with its own memory,
|
||||
/// it must either be a top-level member or a static class method.
|
||||
static Future<MoorIsolate> spawn(DatabaseOpener opener) async {
|
||||
final receiveServer = ReceivePort();
|
||||
final keyFuture = receiveServer.first;
|
||||
|
||||
await Isolate.spawn(_startMoorIsolate, [receiveServer.sendPort, opener]);
|
||||
final key = await keyFuture as ServerKey;
|
||||
return MoorIsolate._(key);
|
||||
}
|
||||
|
||||
/// Creates a [MoorIsolate] in the [Isolate.current] isolate. The returned
|
||||
/// [MoorIsolate] is an object than can be sent across isolates - any other
|
||||
/// isolate can then use [MoorIsolate.connect] to obtain a special database
|
||||
/// connection which operations are all executed on this isolate.
|
||||
factory MoorIsolate.inCurrent(DatabaseOpener opener) {
|
||||
final server = _MoorServer(opener);
|
||||
return MoorIsolate._(server.key);
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a [_MoorServer] and sends the resulting [ServerKey] over a
|
||||
/// [SendPort]. The [args] param must have two parameters, the first one being
|
||||
/// a [SendPort] and the second one being a [DatabaseOpener].
|
||||
void _startMoorIsolate(List args) {
|
||||
final sendPort = args[0] as SendPort;
|
||||
final opener = args[1] as DatabaseOpener;
|
||||
|
||||
final server = _MoorServer(opener);
|
||||
sendPort.send(server.key);
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
part of 'moor_isolate.dart';
|
||||
|
||||
/// A request without further parameters
|
||||
enum _NoArgsRequest {
|
||||
/// Sent from the client to the server. The server will reply with the
|
||||
/// [SqlTypeSystem] of the [_MoorServer.connection] it's managing.
|
||||
getTypeSystem,
|
||||
|
||||
/// Sent from the client to the server. The server will reply with
|
||||
/// [QueryExecutor.ensureOpen], based on the [_MoorServer.connection].
|
||||
ensureOpen,
|
||||
|
||||
/// Sent from the server to a client. The client should run the on create
|
||||
/// method of the attached database
|
||||
runOnCreate,
|
||||
|
||||
/// Sent from the client to start a transaction. The server must reply with an
|
||||
/// integer, which serves as an identifier for the transaction in
|
||||
/// [_ExecuteQuery.transactionId].
|
||||
startTransaction,
|
||||
|
||||
/// Close the background isolate, disconnect all clients, release all
|
||||
/// associated resources
|
||||
terminateAll,
|
||||
}
|
||||
|
||||
enum _StatementMethod {
|
||||
custom,
|
||||
deleteOrUpdate,
|
||||
insert,
|
||||
select,
|
||||
}
|
||||
|
||||
enum _TransactionControl {
|
||||
commit,
|
||||
rollback,
|
||||
}
|
||||
|
||||
/// Sent from the client to run a sql query. The server replies with the
|
||||
/// result.
|
||||
class _ExecuteQuery {
|
||||
final _StatementMethod method;
|
||||
final String sql;
|
||||
final List<dynamic> args;
|
||||
final int transactionId;
|
||||
|
||||
_ExecuteQuery(this.method, this.sql, this.args, [this.transactionId]);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '$method: $sql with $args';
|
||||
}
|
||||
}
|
||||
|
||||
/// Sent from the client to run a list of [BatchedStatement]s.
|
||||
class _ExecuteBatchedStatement {
|
||||
final List<BatchedStatement> stmts;
|
||||
|
||||
_ExecuteBatchedStatement(this.stmts);
|
||||
}
|
||||
|
||||
/// Sent from the client to commit or rollback a transaction
|
||||
class _RunTransactionAction {
|
||||
final _TransactionControl control;
|
||||
final int transactionId;
|
||||
|
||||
_RunTransactionAction(this.control, this.transactionId);
|
||||
}
|
||||
|
||||
/// Sent from the client to notify the server of the
|
||||
/// [GeneratedDatabase.schemaVersion] used by the attached database.
|
||||
class _SetSchemaVersion {
|
||||
final int schemaVersion;
|
||||
|
||||
_SetSchemaVersion(this.schemaVersion);
|
||||
}
|
||||
|
||||
/// Sent from the server to the client. The client should run a database upgrade
|
||||
/// migration.
|
||||
class _RunOnUpgrade {
|
||||
final int versionBefore;
|
||||
final int versionNow;
|
||||
|
||||
_RunOnUpgrade(this.versionBefore, this.versionNow);
|
||||
}
|
||||
|
||||
/// Sent from the server to the client when it should run the before open
|
||||
/// callback.
|
||||
class _RunBeforeOpen {
|
||||
final OpeningDetails details;
|
||||
|
||||
_RunBeforeOpen(this.details);
|
||||
}
|
||||
|
||||
/// Sent to notify that a previous query has updated some tables. When a server
|
||||
/// receives this message, it replies with `null` but forwards a new request
|
||||
/// with this payload to all connected clients.
|
||||
class _NotifyTablesUpdated {
|
||||
final List<String> updatedTables;
|
||||
|
||||
_NotifyTablesUpdated(this.updatedTables);
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
part of 'moor_isolate.dart';
|
||||
|
||||
class _MoorServer {
|
||||
final Server server;
|
||||
|
||||
DatabaseConnection connection;
|
||||
final Map<int, TransactionExecutor> _transactions = {};
|
||||
int _currentTransaction = 0;
|
||||
_FakeDatabase _fakeDb;
|
||||
|
||||
ServerKey get key => server.key;
|
||||
|
||||
_MoorServer(DatabaseOpener opener) : server = Server() {
|
||||
server.openedConnections.listen((connection) {
|
||||
connection.setRequestHandler(_handleRequest);
|
||||
});
|
||||
connection = opener();
|
||||
|
||||
_fakeDb = _FakeDatabase(connection, this);
|
||||
connection.executor.databaseInfo = _fakeDb;
|
||||
}
|
||||
|
||||
/// Returns the first connected client, or null if no client is connected.
|
||||
IsolateCommunication get firstClient {
|
||||
final channels = server.currentChannels;
|
||||
return channels.isEmpty ? null : channels.first;
|
||||
}
|
||||
|
||||
dynamic _handleRequest(Request r) {
|
||||
final payload = r.payload;
|
||||
|
||||
if (payload is _NoArgsRequest) {
|
||||
switch (payload) {
|
||||
case _NoArgsRequest.getTypeSystem:
|
||||
return connection.typeSystem;
|
||||
case _NoArgsRequest.ensureOpen:
|
||||
return connection.executor.ensureOpen();
|
||||
case _NoArgsRequest.startTransaction:
|
||||
return _spawnTransaction();
|
||||
case _NoArgsRequest.terminateAll:
|
||||
connection.executor.close();
|
||||
server.close();
|
||||
Isolate.current.kill();
|
||||
break;
|
||||
// the following are requests which are handled on the client side
|
||||
case _NoArgsRequest.runOnCreate:
|
||||
throw UnsupportedError(
|
||||
'This operation needs to be run on the client');
|
||||
}
|
||||
} else if (payload is _SetSchemaVersion) {
|
||||
_fakeDb.schemaVersion = payload.schemaVersion;
|
||||
return null;
|
||||
} else if (payload is _ExecuteQuery) {
|
||||
return _runQuery(
|
||||
payload.method, payload.sql, payload.args, payload.transactionId);
|
||||
} else if (payload is _ExecuteBatchedStatement) {
|
||||
return connection.executor.runBatched(payload.stmts);
|
||||
} else if (payload is _NotifyTablesUpdated) {
|
||||
for (final connected in server.currentChannels) {
|
||||
connected.request(payload);
|
||||
}
|
||||
} else if (payload is _RunTransactionAction) {
|
||||
return _transactionControl(payload.control, payload.transactionId);
|
||||
}
|
||||
}
|
||||
|
||||
Future<dynamic> _runQuery(
|
||||
_StatementMethod method, String sql, List args, int transactionId) {
|
||||
final executor = transactionId != null
|
||||
? _transactions[transactionId]
|
||||
: connection.executor;
|
||||
|
||||
switch (method) {
|
||||
case _StatementMethod.custom:
|
||||
return executor.runCustom(sql, args);
|
||||
case _StatementMethod.deleteOrUpdate:
|
||||
return executor.runDelete(sql, args);
|
||||
case _StatementMethod.insert:
|
||||
return executor.runInsert(sql, args);
|
||||
case _StatementMethod.select:
|
||||
return executor.runSelect(sql, args);
|
||||
}
|
||||
|
||||
throw AssertionError("Unknown _StatementMethod, this can't happen.");
|
||||
}
|
||||
|
||||
int _spawnTransaction() {
|
||||
final id = _currentTransaction++;
|
||||
_transactions[id] = connection.executor.beginTransaction();
|
||||
return id;
|
||||
}
|
||||
|
||||
Future<void> _transactionControl(
|
||||
_TransactionControl action, int transactionId) {
|
||||
final transaction = _transactions[transactionId];
|
||||
_transactions.remove(transactionId);
|
||||
switch (action) {
|
||||
case _TransactionControl.commit:
|
||||
return transaction.send();
|
||||
case _TransactionControl.rollback:
|
||||
return transaction.rollback();
|
||||
}
|
||||
throw AssertionError("Can't happen");
|
||||
}
|
||||
}
|
||||
|
||||
/// A mock database so that the [QueryExecutor] which is running on a background
|
||||
/// isolate can have the [QueryExecutor.databaseInfo] set. The query executor
|
||||
/// uses that to set the schema version and to run migration callbacks. For a
|
||||
/// server, all of that is delegated via clients.
|
||||
class _FakeDatabase extends GeneratedDatabase {
|
||||
final _MoorServer server;
|
||||
|
||||
_FakeDatabase(DatabaseConnection connection, this.server)
|
||||
: super.connect(connection);
|
||||
|
||||
@override
|
||||
final List<TableInfo<Table, DataClass>> allTables = const [];
|
||||
|
||||
@override
|
||||
int schemaVersion = 0; // will be overridden by client requests
|
||||
|
||||
@override
|
||||
Future<void> handleDatabaseCreation({SqlExecutor executor}) {
|
||||
return server.firstClient.request(_NoArgsRequest.runOnCreate);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> handleDatabaseVersionChange(
|
||||
{SqlExecutor executor, int from, int to}) {
|
||||
return server.firstClient.request(_RunOnUpgrade(from, to));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> beforeOpenCallback(
|
||||
QueryExecutor executor, OpeningDetails details) {
|
||||
return server.firstClient.request(_RunBeforeOpen(details));
|
||||
}
|
||||
}
|
|
@ -1,9 +1,7 @@
|
|||
import 'package:moor/moor.dart';
|
||||
import 'package:moor/src/runtime/components/component.dart';
|
||||
import 'package:moor/src/runtime/expressions/expression.dart';
|
||||
part of '../query_builder.dart';
|
||||
|
||||
/// A type for a [Join] (e.g. inner, outer).
|
||||
enum JoinType {
|
||||
enum _JoinType {
|
||||
/// Perform an inner join, see the [innerJoin] function for details.
|
||||
inner,
|
||||
|
||||
|
@ -14,27 +12,30 @@ enum JoinType {
|
|||
cross
|
||||
}
|
||||
|
||||
const Map<JoinType, String> _joinKeywords = {
|
||||
JoinType.inner: 'INNER',
|
||||
JoinType.leftOuter: 'LEFT OUTER',
|
||||
JoinType.cross: 'CROSS',
|
||||
const Map<_JoinType, String> _joinKeywords = {
|
||||
_JoinType.inner: 'INNER',
|
||||
_JoinType.leftOuter: 'LEFT OUTER',
|
||||
_JoinType.cross: 'CROSS',
|
||||
};
|
||||
|
||||
/// Used internally by moor when calling [SimpleSelectStatement.join].
|
||||
///
|
||||
/// You should use [innerJoin], [leftOuterJoin] or [crossJoin] to obtain a
|
||||
/// [Join] instance.
|
||||
class Join<T extends Table, D extends DataClass> extends Component {
|
||||
/// The [JoinType] of this join.
|
||||
final JoinType type;
|
||||
/// The [_JoinType] of this join.
|
||||
final _JoinType type;
|
||||
|
||||
/// The [TableInfo] that will be added to the query
|
||||
final TableInfo<T, D> table;
|
||||
|
||||
/// For joins that aren't [JoinType.cross], contains an additional predicate
|
||||
/// For joins that aren't [_JoinType.cross], contains an additional predicate
|
||||
/// that must be matched for the join.
|
||||
final Expression<bool, BoolType> on;
|
||||
|
||||
/// Constructs a [Join] by providing the relevant fields. [on] is optional for
|
||||
/// [JoinType.cross].
|
||||
Join(this.type, this.table, this.on);
|
||||
/// [_JoinType.cross].
|
||||
Join._(this.type, this.table, this.on);
|
||||
|
||||
@override
|
||||
void writeInto(GenerationContext context) {
|
||||
|
@ -43,7 +44,7 @@ class Join<T extends Table, D extends DataClass> extends Component {
|
|||
|
||||
context.buffer.write(table.tableWithAlias);
|
||||
|
||||
if (type != JoinType.cross) {
|
||||
if (type != _JoinType.cross) {
|
||||
context.buffer.write(' ON ');
|
||||
on.writeInto(context);
|
||||
}
|
||||
|
@ -56,7 +57,7 @@ class Join<T extends Table, D extends DataClass> extends Component {
|
|||
/// - http://www.sqlitetutorial.net/sqlite-inner-join/
|
||||
Join innerJoin<T extends Table, D extends DataClass>(
|
||||
TableInfo<T, D> other, Expression<bool, BoolType> on) {
|
||||
return Join(JoinType.inner, other, on);
|
||||
return Join._(_JoinType.inner, other, on);
|
||||
}
|
||||
|
||||
/// Creates a sql left outer join that can be used in
|
||||
|
@ -66,7 +67,7 @@ Join innerJoin<T extends Table, D extends DataClass>(
|
|||
/// - http://www.sqlitetutorial.net/sqlite-left-join/
|
||||
Join leftOuterJoin<T extends Table, D extends DataClass>(
|
||||
TableInfo<T, D> other, Expression<bool, BoolType> on) {
|
||||
return Join(JoinType.leftOuter, other, on);
|
||||
return Join._(_JoinType.leftOuter, other, on);
|
||||
}
|
||||
|
||||
/// Creates a sql cross join that can be used in
|
||||
|
@ -75,5 +76,5 @@ Join leftOuterJoin<T extends Table, D extends DataClass>(
|
|||
/// See also:
|
||||
/// - http://www.sqlitetutorial.net/sqlite-cross-join/
|
||||
Join crossJoin<T, D>(TableInfo other) {
|
||||
return Join(JoinType.cross, other, null);
|
||||
return Join._(_JoinType.cross, other, null);
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import 'package:moor/src/runtime/components/component.dart';
|
||||
part of '../query_builder.dart';
|
||||
|
||||
/// A limit clause inside a select, update or delete statement.
|
||||
class Limit extends Component {
|
|
@ -1,6 +1,4 @@
|
|||
import 'package:meta/meta.dart';
|
||||
import 'package:moor/src/runtime/components/component.dart';
|
||||
import 'package:moor/src/runtime/expressions/expression.dart';
|
||||
part of '../query_builder.dart';
|
||||
|
||||
/// Describes how to order rows
|
||||
enum OrderingMode {
|
||||
|
@ -29,6 +27,16 @@ class OrderingTerm extends Component {
|
|||
/// ascending).
|
||||
OrderingTerm({@required this.expression, this.mode = OrderingMode.asc});
|
||||
|
||||
/// Creates an ordering term that sorts for ascending values of [expression].
|
||||
factory OrderingTerm.asc(Expression expression) {
|
||||
return OrderingTerm(expression: expression, mode: OrderingMode.asc);
|
||||
}
|
||||
|
||||
/// Creates an ordering term that sorts for descending values of [expression].
|
||||
factory OrderingTerm.desc(Expression expression) {
|
||||
return OrderingTerm(expression: expression, mode: OrderingMode.desc);
|
||||
}
|
||||
|
||||
@override
|
||||
void writeInto(GenerationContext context) {
|
||||
expression.writeInto(context);
|
||||
|
@ -58,7 +66,7 @@ class OrderBy extends Component {
|
|||
context.buffer.write('ORDER BY ');
|
||||
}
|
||||
|
||||
for (var term in terms) {
|
||||
for (final term in terms) {
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue