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:
Simon Binder 2019-12-30 21:08:32 +01:00
commit ba603f22cc
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
426 changed files with 12732 additions and 4880 deletions

View File

@ -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

8
.gitignore vendored
View File

@ -1,4 +1,10 @@
**/.idea
**/*.iml
lcov.info
lcov.info
.packages
pubspec.lock
.dart_tool/
benchmark_results.json

View File

@ -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`.

View File

@ -1,21 +1,48 @@
# Moor
[![Build Status](https://api.cirrus-ci.com/github/simolus3/moor.svg)](https://cirrus-ci.com/github/simolus3/moor)
[![codecov](https://codecov.io/gh/simolus3/moor/branch/master/graph/badge.svg)](https://codecov.io/gh/simolus3/moor)
[![Chat on Gitter](https://img.shields.io/gitter/room/moor-dart/community)](https://gitter.im/moor-dart/community)
| Core | Flutter | Generator |
|:-------------:|:-------------:|:-----:|
| [![Generator version](https://img.shields.io/pub/v/moor.svg)](https://pub.dev/packages/moor) | [![Flutter version](https://img.shields.io/pub/v/moor_flutter.svg)](https://pub.dev/packages/moor_flutter) | [![Generator version](https://img.shields.io/pub/v/moor_generator.svg)](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
[![sqlparser](https://img.shields.io/pub/v/sqlparser.svg)](https://pub.dev/packages/sqlparser)

View File

@ -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)

View File

@ -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"

View File

@ -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 >}}

View File

@ -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

View File

@ -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.

View File

@ -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:

View File

@ -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.

View File

@ -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 [![Flutter version](https://img.shields.io/pub/v/moor_flutter.svg)](https://pub.dartlang.org/packages/moor_flutter) and the current version of `moor_generator` is [![Generator version](https://img.shields.io/pub/v/moor_generator.svg)](https://pub.dartlang.org/packages/moor_generator)
At the moment, the current version of `moor` is [![Moor version](https://img.shields.io/pub/v/moor.svg)](https://pub.dartlang.org/packages/moor),
`moor_ffi` is at [![Moor version](https://img.shields.io/pub/v/moor_ffi.svg)](https://pub.dartlang.org/packages/moor_ffi)
and the latest version of `moor_generator` is [![Generator version](https://img.shields.io/pub/v/moor_generator.svg)](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!

View File

@ -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.

View File

@ -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.

View File

@ -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 [![Flutter version](https://img.shields.io/pub/v/moor_flutter.svg)](https://pub.dartlang.org/packages/moor_flutter) and the current version of `moor_generator` is [![Generator version](https://img.shields.io/pub/v/moor_generator.svg)](https://pub.dartlang.org/packages/moor_generator)
At the moment, the current version of `moor` is [![Moor version](https://img.shields.io/pub/v/moor.svg)](https://pub.dartlang.org/packages/moor),
`moor_ffi` is at [![Moor version](https://img.shields.io/pub/v/moor_ffi.svg)](https://pub.dartlang.org/packages/moor_ffi)
and the latest version of `moor_generator` is [![Generator version](https://img.shields.io/pub/v/moor_generator.svg)](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.

View File

@ -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.

View File

@ -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).

View File

@ -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).

View File

@ -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.

View File

@ -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).

View File

@ -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.

View File

@ -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

View File

@ -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>

View File

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

View File

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

View File

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

View File

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

View 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];
}

View File

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

View File

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

View File

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

View 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();
}
}

View File

@ -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

View File

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

View File

@ -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');
}

View File

@ -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

View File

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

View File

@ -1,2 +1,3 @@
org.gradle.jvmargs=-Xmx1536M
android.enableR8=true

View File

@ -26,7 +26,7 @@ class FfiExecutor extends TestExecutor {
}
}
void main() async {
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final dbPath = await getDatabasesPath();

View File

@ -28,7 +28,7 @@ class SqfliteExecutor extends TestExecutor {
}
}
void main() async {
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
runAllTests(SqfliteExecutor());

View File

@ -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"

View File

@ -0,0 +1,4 @@
analyzer:
strong-mode:
implicit-casts: false
implicit-dynamic: false

View File

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

View File

@ -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) {

View File

@ -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 &&

View File

@ -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)
],
),
);

View File

@ -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);

View File

@ -14,6 +14,8 @@ abstract class TestExecutor {
}
void runAllTests(TestExecutor executor) {
moorRuntimeOptions.dontWarnAboutMultipleDatabases = true;
tearDown(() async {
await executor.deleteData();
});

View File

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

View File

@ -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:

View File

@ -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() {

View File

@ -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() {

View File

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

View File

@ -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];
}

View File

@ -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>

View File

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

View File

@ -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:

View File

@ -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

3
moor/.gitignore vendored
View File

@ -32,8 +32,7 @@ android/
ios/
# coverage
test/.test_coverage.dart
coverage_badge.svg
coverage/
### Intellij ###
.idea/**/*

View File

@ -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:

View File

@ -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.

View File

@ -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

2
moor/dart_test.yaml Normal file
View File

@ -0,0 +1,2 @@
tags:
integration:

View File

@ -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 &&

View File

@ -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;

View File

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

5
moor/lib/isolate.dart Normal file
View File

@ -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';

View File

@ -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';

View File

@ -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

View File

@ -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 {}});
}

38
moor/lib/src/dsl/dsl.dart Normal file
View File

@ -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?',
);
}

View File

@ -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

View File

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

View File

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

View File

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

View File

@ -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]--;
}
}
}

View File

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

View File

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

View File

@ -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;

View File

@ -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';
}
}

View File

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

View File

@ -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 &&

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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)');
}
}

View File

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

View File

@ -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',
};
}

View File

@ -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';

View File

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

View File

@ -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';
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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 {

View File

@ -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