Merge branch 'develop' into multiple-isolates

This commit is contained in:
Simon Binder 2019-10-28 20:42:30 +01:00
commit ebc22c8382
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
111 changed files with 1906 additions and 916 deletions

View File

@ -1,7 +1,7 @@
# 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 |
|:-------------:|:-------------:|:-----:|

View File

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

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

@ -41,6 +41,13 @@ 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 does moor compare to X?
There are a variety of good persistence libraries for Dart and Flutter.

View File

@ -0,0 +1,110 @@
---
title: "Testing"
description: Guide on writing unit tests for moor databases
---
Flutter apps using moor can always be tested with [integration tests](https://flutter.dev/docs/cookbook/testing/integration/introduction)
running on a real device. This guide focusses on writing unit tests for a database written in moor.
Those tests can be run and debugged on your computer without additional setup, you don't a
physical device to run them.
## Setup
In addition to the moor dependencies you already have, add `moor_ffi` as a dev dependency. If you're already using `moor_ffi`
as a regular dependency, you can skip this.
```yaml
dev_dependencies:
moor_ffi: ^0.1.0 # or latest version
```
For this guide, we're going to test a very simple database that stores user names. The only important change from a regular moor
database is the constructor: We make the `QueryExecutor` argument explicit instead of having a no-args constructor that passes
a `FlutterQueryExector` to the superclass.
```dart
import 'package:moor/moor.dart';
part 'database.g.dart';
class Users extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text()();
}
@UseMoor(tables: [Users])
class MyDatabase extends _$MyDatabase {
MyDatabase(QueryExecutor e) : super(e);
@override
int get schemaVersion => 1;
/// Creates a user and returns their id
Future<int> createUser(String name) {
return into(users).insert(UsersCompanion.insert(name: name));
}
/// Changes the name of a user with the [id] to the [newName].
Future<void> updateName(int id, String newName) {
return update(users).replace(User(id: id, name: newName));
}
Stream<User> watchUserWithId(int id) {
return (select(users)..where((u) => u.id.equals(id))).watchSingle();
}
}
```
{{% 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
`VmDatabase.memory()` instead of a `FlutterQueryExecutor`. A good
place to open the database is the `setUp` and `tearDown` methods from
`package:test`:
```dart
import 'package:moor_ffi/moor_ffi.dart';
import 'package:test/test.dart';
// the file defined above, you can test any moor database of course
import 'database.dart';
void main() {
MyDatabase database;
setUp(() {
database = MyDatabase(VmDatabase.memory());
});
tearDown(() async {
await database.close();
});
}
```
With that setup in place, we can finally write some tests:
```dart
test('users can be created', () async {
final id = await database.createUser('some user');
final user = await database.watchUserWithId(id).first;
expect(user.name, 'some user');
});
test('stream emits a new user when the name updates', () async {
final id = await database.createUser('first name');
final expectation = expectLater(
database.watchUserWithId(id).map((user) => user.name),
emitsInOrder(['first name', 'changed name']),
);
await database.updateName(id, 'changed name');
await expectation;
});
```

View File

@ -1,3 +1,7 @@
## unreleased
- Fix crash when `customStatement` is the first operation used on a database ([#199](https://github.com/simolus3/moor/issues/199))
## 2.0.1
- Introduced `isBetween` and `isBetweenValues` methods for comparable expressions (int, double, datetime)

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

@ -7,31 +7,15 @@ 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/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/types/sql_types.dart';
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,7 +1,4 @@
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.

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

View File

@ -0,0 +1,8 @@
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';

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 {

View File

@ -2,13 +2,8 @@ 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';
const _zoneRootUserKey = #DatabaseConnectionUser;
@ -193,17 +188,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>(
@ -307,7 +328,11 @@ mixin QueryEngine on DatabaseConnectionUser {
@protected
@visibleForTesting
Future<void> customStatement(String statement, [List<dynamic> args]) {
return _resolvedEngine.executor.runCustom(statement, args);
final engine = _resolvedEngine;
return engine.executor.doWhenOpened((executor) {
return executor.runCustom(statement, args);
});
}
/// Executes [action] in a transaction, which means that all its queries and

View File

@ -1,7 +1,7 @@
import 'dart:async';
import 'package:collection/collection.dart';
import 'package:moor/src/runtime/components/component.dart';
import 'package:moor/backends.dart';
import 'package:moor/src/runtime/database.dart';
import 'package:moor/src/utils/hash.dart';

View File

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

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

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

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

View File

@ -1,6 +1,4 @@
import 'package:moor/src/runtime/components/component.dart';
import 'package:moor/src/runtime/expressions/expression.dart';
import 'package:moor/src/types/sql_types.dart';
part of '../query_builder.dart';
/// A where clause in a select, update or delete statement.
class Where extends Component {

View File

@ -1,6 +1,4 @@
import 'package:moor/src/runtime/components/component.dart';
import 'package:moor/src/runtime/expressions/expression.dart';
import 'package:moor/src/types/sql_types.dart';
part of '../query_builder.dart';
/// Returns an expression that is true iff both [a] and [b] are true.
Expression<bool, BoolType> and(
@ -16,7 +14,7 @@ Expression<bool, BoolType> or(
Expression<bool, BoolType> not(Expression<bool, BoolType> a) =>
_NotExpression(a);
class _AndExpression extends InfixOperator<bool, BoolType> {
class _AndExpression extends _InfixOperator<bool, BoolType> {
@override
Expression<bool, BoolType> left, right;
@ -26,7 +24,7 @@ class _AndExpression extends InfixOperator<bool, BoolType> {
_AndExpression(this.left, this.right);
}
class _OrExpression extends InfixOperator<bool, BoolType> {
class _OrExpression extends _InfixOperator<bool, BoolType> {
@override
Expression<bool, BoolType> left, right;

View File

@ -1,6 +1,4 @@
import 'package:moor/moor.dart';
import 'package:moor/src/runtime/components/component.dart';
import 'expression.dart';
part of '../query_builder.dart';
// todo: Can we replace these classes with an extension on expression?
@ -23,7 +21,7 @@ mixin ComparableExpr<DT, ST extends SqlType<DT>> on Expression<DT, ST> {
/// Returns an expression that is true if this expression is strictly bigger
/// than the other expression.
Expression<bool, BoolType> isBiggerThan(Expression<DT, ST> other) {
return Comparison(this, ComparisonOperator.more, other);
return _Comparison(this, _ComparisonOperator.more, other);
}
/// Returns an expression that is true if this expression is strictly bigger
@ -34,7 +32,7 @@ mixin ComparableExpr<DT, ST extends SqlType<DT>> on Expression<DT, ST> {
/// Returns an expression that is true if this expression is bigger than or
/// equal to he other expression.
Expression<bool, BoolType> isBiggerOrEqual(Expression<DT, ST> other) {
return Comparison(this, ComparisonOperator.moreOrEqual, other);
return _Comparison(this, _ComparisonOperator.moreOrEqual, other);
}
/// Returns an expression that is true if this expression is bigger than or
@ -45,7 +43,7 @@ mixin ComparableExpr<DT, ST extends SqlType<DT>> on Expression<DT, ST> {
/// Returns an expression that is true if this expression is strictly smaller
/// than the other expression.
Expression<bool, BoolType> isSmallerThan(Expression<DT, ST> other) {
return Comparison(this, ComparisonOperator.less, other);
return _Comparison(this, _ComparisonOperator.less, other);
}
/// Returns an expression that is true if this expression is strictly smaller
@ -56,7 +54,7 @@ mixin ComparableExpr<DT, ST extends SqlType<DT>> on Expression<DT, ST> {
/// Returns an expression that is true if this expression is smaller than or
/// equal to he other expression.
Expression<bool, BoolType> isSmallerOrEqual(Expression<DT, ST> other) {
return Comparison(this, ComparisonOperator.lessOrEqual, other);
return _Comparison(this, _ComparisonOperator.lessOrEqual, other);
}
/// Returns an expression that is true if this expression is smaller than or

View File

@ -1,6 +1,4 @@
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 custom expression that can appear in a sql statement.
/// The [CustomExpression.content] will be written into the query without any

View File

@ -1,7 +1,4 @@
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';
part of '../query_builder.dart';
/// Extracts the (UTC) year from the given expression that resolves
/// to a datetime.

View File

@ -1,7 +1,4 @@
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';
part of '../query_builder.dart';
/// Any sql expression that evaluates to some generic value. This does not
/// include queries (which might evaluate to multiple values) but individual
@ -16,19 +13,20 @@ abstract class Expression<D, T extends SqlType<D>> implements Component {
/// Whether this expression is equal to the given expression.
Expression<bool, BoolType> equalsExp(Expression<D, T> compare) =>
Comparison.equal(this, 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));
_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> {
abstract class _InfixOperator<D, T extends SqlType<D>>
extends Expression<D, T> {
/// The left-hand side of this expression
Expression get left;
@ -40,7 +38,6 @@ abstract class InfixOperator<D, T extends SqlType<D>> extends Expression<D, T> {
/// Whether we should put parentheses around the [left] and [right]
/// expressions.
@visibleForOverriding
bool get placeBrackets => true;
@override
@ -65,8 +62,8 @@ abstract class InfixOperator<D, T extends SqlType<D>> extends Expression<D, T> {
}
}
/// Defines the possible comparison operators that can appear in a [Comparison].
enum ComparisonOperator {
/// Defines the possible comparison operators that can appear in a [_Comparison].
enum _ComparisonOperator {
/// '<' in sql
less,
@ -84,13 +81,13 @@ enum ComparisonOperator {
}
/// 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: '>'
class _Comparison extends _InfixOperator<bool, BoolType> {
static const Map<_ComparisonOperator, String> _operatorNames = {
_ComparisonOperator.less: '<',
_ComparisonOperator.lessOrEqual: '<=',
_ComparisonOperator.equal: '=',
_ComparisonOperator.moreOrEqual: '>=',
_ComparisonOperator.more: '>'
};
@override
@ -99,7 +96,7 @@ class Comparison extends InfixOperator<bool, BoolType> {
final Expression right;
/// The operator to use for this comparison
final ComparisonOperator op;
final _ComparisonOperator op;
@override
final bool placeBrackets = false;
@ -109,8 +106,8 @@ class Comparison extends InfixOperator<bool, BoolType> {
/// Constructs a comparison from the [left] and [right] expressions to compare
/// and the [ComparisonOperator] [op].
Comparison(this.left, this.op, this.right);
_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;
/// Like [Comparison(left, op, right)], but uses [_ComparisonOperator.equal].
_Comparison.equal(this.left, this.right) : op = _ComparisonOperator.equal;
}

View File

@ -1,7 +1,4 @@
import 'package:moor/moor.dart';
import 'package:moor/src/runtime/components/component.dart';
import 'package:moor/src/runtime/expressions/expression.dart';
import 'package:moor/src/types/sql_types.dart';
part of '../query_builder.dart';
/// An expression that is true if the given [expression] resolves to any of the
/// values in [values].

View File

@ -1,6 +1,4 @@
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';
/// Expression that is true if the inner expression resolves to a null value.
Expression<bool, BoolType> isNull(Expression inner) => _NullCheck(inner, true);

View File

@ -1,10 +1,8 @@
import 'package:moor/src/runtime/components/component.dart';
import 'package:moor/src/runtime/expressions/expression.dart';
import 'package:moor/src/types/sql_types.dart';
part of '../query_builder.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> {
class _LikeOperator extends Expression<bool, BoolType> {
/// The target expression that will be tested
final Expression<String, StringType> target;
@ -12,7 +10,7 @@ class LikeOperator extends Expression<bool, BoolType> {
final Expression<String, StringType> regex;
/// Perform a like operator with the target and the regex.
LikeOperator(this.target, this.regex);
_LikeOperator(this.target, this.regex);
@override
void writeInto(GenerationContext context) {
@ -44,7 +42,7 @@ enum Collate {
}
/// A `text COLLATE collate` expression in sqlite.
class CollateOperator extends Expression<String, StringType> {
class _CollateOperator extends Expression<String, StringType> {
/// The expression on which the collate function will be run
final Expression inner;
@ -53,7 +51,7 @@ class CollateOperator extends Expression<String, StringType> {
/// Constructs a collate expression on the [inner] expression and the
/// [Collate].
CollateOperator(this.inner, this.collate);
_CollateOperator(this.inner, this.collate);
@override
void writeInto(GenerationContext context) {

View File

@ -1,8 +1,4 @@
import 'dart:typed_data';
import 'package:moor/src/runtime/components/component.dart';
import 'package:moor/src/runtime/expressions/expression.dart';
import 'package:moor/src/types/sql_types.dart';
part of '../query_builder.dart';
/// An expression that represents the value of a dart object encoded to sql
/// using prepared statements.

View File

@ -1,26 +1,4 @@
import 'package:moor/moor.dart';
/// A component is anything that can appear in a sql query.
abstract class Component {
/// Writes this component into the [context] by writing to its
/// [GenerationContext.buffer] or by introducing bound variables. When writing
/// into the buffer, no whitespace around the this component should be
/// introduced. When a component consists of multiple composed component, it's
/// responsible for introducing whitespace between its child components.
void writeInto(GenerationContext context);
}
/// An enumeration of database systems supported by moor. Only
/// [SqlDialect.sqlite] is officially supported, all others are in an
/// experimental state at the moment.
enum SqlDialect {
/// Use sqlite's sql dialect. This is the default option and the only
/// officially supported dialect at the moment.
sqlite,
/// (currently unsupported)
mysql
}
part of 'query_builder.dart';
/// Contains information about a query while it's being constructed.
class GenerationContext {

View File

@ -1,9 +1,4 @@
import 'dart:async';
import 'package:moor/moor.dart';
import 'package:moor/src/runtime/components/component.dart';
import 'package:moor/src/runtime/structure/columns.dart';
import 'package:moor/src/runtime/structure/table_info.dart';
part of 'query_builder.dart';
/// Signature of a function that will be invoked when a database is created.
typedef Future<void> OnCreate(Migrator m);

View File

@ -0,0 +1,59 @@
// Mega compilation unit that includes all Dart apis related to generating SQL
// at runtime.
import 'package:meta/meta.dart';
import 'package:moor/moor.dart';
import 'package:moor/sqlite_keywords.dart';
import 'package:moor/src/runtime/executor/stream_queries.dart';
import 'package:moor/src/utils/single_transformer.dart';
part 'components/join.dart';
part 'components/limit.dart';
part 'components/order_by.dart';
part 'components/where.dart';
part 'expressions/bools.dart';
part 'expressions/comparable.dart';
part 'expressions/custom.dart';
part 'expressions/datetimes.dart';
part 'expressions/expression.dart';
part 'expressions/in.dart';
part 'expressions/null_check.dart';
part 'expressions/text.dart';
part 'expressions/variables.dart';
part 'schema/columns.dart';
part 'schema/table_info.dart';
part 'statements/select/custom_select.dart';
part 'statements/select/select.dart';
part 'statements/select/select_with_join.dart';
part 'statements/delete.dart';
part 'statements/insert.dart';
part 'statements/query.dart';
part 'statements/update.dart';
part 'generation_context.dart';
part 'migration.dart';
/// A component is anything that can appear in a sql query.
abstract class Component {
/// Writes this component into the [context] by writing to its
/// [GenerationContext.buffer] or by introducing bound variables. When writing
/// into the buffer, no whitespace around the this component should be
/// introduced. When a component consists of multiple composed component, it's
/// responsible for introducing whitespace between its child components.
void writeInto(GenerationContext context);
}
/// An enumeration of database systems supported by moor. Only
/// [SqlDialect.sqlite] is officially supported, all others are in an
/// experimental state at the moment.
enum SqlDialect {
/// Use sqlite's sql dialect. This is the default option and the only
/// officially supported dialect at the moment.
sqlite,
/// (currently unsupported)
mysql
}

View File

@ -1,15 +1,4 @@
import 'dart:typed_data';
import 'package:meta/meta.dart';
import 'package:moor/moor.dart';
import 'package:moor/src/runtime/components/component.dart';
import 'package:moor/src/runtime/expressions/expression.dart';
import 'package:moor/src/runtime/expressions/text.dart';
import 'package:moor/src/runtime/expressions/variables.dart';
import 'package:moor/src/types/sql_types.dart';
import 'package:moor/sqlite_keywords.dart';
import 'error_handling.dart';
part of '../query_builder.dart';
const VerificationResult _invalidNull = VerificationResult.failure(
"This column is not nullable and doesn't have a default value. "
@ -137,11 +126,11 @@ class GeneratedTextColumn extends GeneratedColumn<String, StringType>
@override
Expression<bool, BoolType> like(String pattern) =>
LikeOperator(this, Variable<String, StringType>(pattern));
_LikeOperator(this, Variable<String, StringType>(pattern));
@override
Expression<String, StringType> collate(Collate collate) {
return CollateOperator(this, collate);
return _CollateOperator(this, collate);
}
@override
@ -180,7 +169,7 @@ class GeneratedBoolColumn extends GeneratedColumn<bool, BoolType>
@override
void writeCustomConstraints(StringBuffer into) {
into.write(' CHECK (${$name} in (0, 1))');
into.write(' CHECK ($escapedName in (0, 1))');
}
}

View File

@ -1,5 +1,4 @@
import 'package:moor/moor.dart';
import 'package:moor/src/runtime/expressions/variables.dart';
part of '../query_builder.dart';
/// Base class for generated classes. [TableDsl] is the type specified by the
/// user that extends [Table], [D] is the type of the data class

View File

@ -1,9 +1,4 @@
import 'dart:async';
import 'package:moor/moor.dart';
import 'package:moor/src/runtime/components/component.dart';
import 'package:moor/src/runtime/statements/query.dart';
import 'package:moor/src/runtime/structure/table_info.dart';
part of '../query_builder.dart';
/// A `DELETE` statement in sql
class DeleteStatement<T extends Table, D extends DataClass> extends Query<T, D>

View File

@ -0,0 +1,186 @@
part of '../query_builder.dart';
/// Represents an insert statements
class InsertStatement<D extends DataClass> {
/// The database to use then executing this statement
@protected
final QueryEngine database;
/// The table we're inserting into
@protected
final TableInfo<Table, D> table;
/// Constructs an insert statement from the database and the table. Used
/// internally by moor.
InsertStatement(this.database, this.table);
/// Inserts a row constructed from the fields in [entity].
///
/// 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].
///
/// If the table contains an auto-increment column, the generated value will
/// be returned. If there is no auto-increment column, you can't rely on the
/// return value, but the future will resolve to an error when the insert
/// fails.
Future<int> insert(
Insertable<D> entity, {
@Deprecated('Use mode: InsertMode.replace instead') bool orReplace = false,
InsertMode mode,
}) async {
assert(
mode == null || (orReplace != true),
'If the mode parameter is set on insertAll, orReplace must be null or '
'false',
);
_validateIntegrity(entity);
final ctx = _createContext(entity, _resolveMode(mode, orReplace));
return await database.executor.doWhenOpened((e) async {
final id = await database.executor.runInsert(ctx.sql, ctx.boundVariables);
database.markTablesUpdated({table});
return id;
});
}
/// 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].
Future<void> insertAll(
List<Insertable<D>> rows, {
@Deprecated('Use mode: InsertMode.replace instead') bool orReplace = false,
InsertMode mode,
}) async {
assert(
mode == null || (orReplace != true),
'If the mode parameter is set on insertAll, orReplace must be null or '
'false',
);
final statements = <String, List<GenerationContext>>{};
// Not every insert has the same sql, as fields which are set to null are
// not included. So, we have a map for sql -> list of variables which we can
// then turn into prepared statements
for (var row in rows) {
_validateIntegrity(row);
final ctx = _createContext(row, _resolveMode(mode, orReplace));
statements.putIfAbsent(ctx.sql, () => []).add(ctx);
}
final batchedStatements = statements.entries.map((e) {
final vars = e.value.map((context) => context.boundVariables).toList();
return BatchedStatement(e.key, vars);
}).toList(growable: false);
await database.executor.doWhenOpened((e) async {
await e.runBatched(batchedStatements);
});
database.markTablesUpdated({table});
}
GenerationContext _createContext(Insertable<D> entry, InsertMode mode) {
final map = table.entityToSql(entry.createCompanion(true))
..removeWhere((_, value) => value == null);
final ctx = GenerationContext.fromDb(database);
ctx.buffer
..write(_insertKeywords[mode])
..write(' INTO ')
..write(table.$tableName)
..write(' ');
if (map.isEmpty) {
ctx.buffer.write('DEFAULT VALUES');
} else {
ctx.buffer
..write('(')
..write(map.keys.join(', '))
..write(') ')
..write('VALUES (');
var first = true;
for (var variable in map.values) {
if (!first) {
ctx.buffer.write(', ');
}
first = false;
variable.writeInto(ctx);
}
ctx.buffer.write(')');
}
return ctx;
}
InsertMode _resolveMode(InsertMode mode, bool orReplace) {
return mode ??
(orReplace == true ? InsertMode.insertOrReplace : InsertMode.insert);
}
void _validateIntegrity(Insertable<D> d) {
if (d == null) {
throw InvalidDataException(
'Cannot write null row into ${table.$tableName}');
}
table
.validateIntegrity(d.createCompanion(true), isInserting: true)
.throwIfInvalid(d);
}
}
/// Enumeration of different insert behaviors. See the documentation on the
/// individual fields for details.
enum InsertMode {
/// A regular `INSERT INTO` statement. When a row with the same primary or
/// unique key already exists, the insert statement will fail and an exception
/// will be thrown. If the exception is caught, previous statements made in
/// the same transaction will NOT be reverted.
insert,
/// Identical to [InsertMode.insertOrReplace], included for the sake of
/// completeness.
replace,
/// Like [insert], but if a row with the same primary or unique key already
/// exists, it will be deleted and re-created with the row being inserted.
insertOrReplace,
/// Similar to [InsertMode.insertOrAbort], but it will revert the surrounding
/// transaction if a constraint is violated, even if the thrown exception is
/// caught.
insertOrRollback,
/// Identical to [insert], included for the sake of completeness.
insertOrAbort,
/// Like [insert], but if multiple values are inserted with the same insert
/// statement and one of them fails, the others will still be completed.
insertOrFail,
/// Like [insert], but failures will be ignored.
insertOrIgnore,
}
const _insertKeywords = <InsertMode, String>{
InsertMode.insert: 'INSERT',
InsertMode.replace: 'REPLACE',
InsertMode.insertOrReplace: 'INSERT OR REPLACE',
InsertMode.insertOrRollback: 'INSERT OR ROLLBACK',
InsertMode.insertOrAbort: 'INSERT OR ABORT',
InsertMode.insertOrFail: 'INSERT OR FAIL',
InsertMode.insertOrIgnore: 'INSERT OR IGNORE',
};

View File

@ -1,12 +1,4 @@
import 'package:meta/meta.dart';
import 'package:moor/moor.dart';
import 'package:moor/src/runtime/components/component.dart';
import 'package:moor/src/runtime/components/limit.dart';
import 'package:moor/src/runtime/components/order_by.dart';
import 'package:moor/src/runtime/components/where.dart';
import 'package:moor/src/runtime/expressions/custom.dart';
import 'package:moor/src/runtime/expressions/expression.dart';
import 'package:moor/src/utils/single_transformer.dart';
part of '../query_builder.dart';
/// Statement that operates with data that already exists (select, delete,
/// update).
@ -161,6 +153,28 @@ class _MappedSelectable<S, T> extends Selectable<T> {
mixin SingleTableQueryMixin<T extends Table, D extends DataClass>
on Query<T, D> {
/// Makes this statement only include rows that match the [filter].
///
/// For instance, if you have a table users with an id column, you could
/// select a user with a specific id by using
/// ```dart
/// (select(users)..where((u) => u.id.equals(42))).watchSingle()
/// ```
///
/// Please note that this [where] call is different to [Iterable.where] and
/// [Stream.where] in the sense that [filter] will NOT be called for each
/// row. Instead, it will only be called once (with the underlying table as
/// parameter). The result [Expression] will be written as a SQL string and
/// sent to the underlying database engine. The filtering does not happen in
/// Dart.
/// If a where condition has already been set before, the resulting filter
/// will be the conjunction of both calls.
///
/// For more information, see:
/// - The docs on [expressions](https://moor.simonbinder.eu/docs/getting-started/expressions/),
/// which explains how to express most SQL expressions in Dart.
/// If you want to remove duplicate rows from a query, use the `distinct`
/// parameter on [QueryEngine.select].
void where(Expression<bool, BoolType> filter(T tbl)) {
final predicate = filter(table.asDslTable);
@ -199,7 +213,7 @@ mixin SingleTableQueryMixin<T extends Table, D extends DataClass>
// custom expression that references the column
final columnExpression = CustomExpression(entry.key);
final comparison =
Comparison(columnExpression, ComparisonOperator.equal, entry.value);
_Comparison(columnExpression, _ComparisonOperator.equal, entry.value);
if (predicate == null) {
predicate = comparison;
@ -212,6 +226,7 @@ mixin SingleTableQueryMixin<T extends Table, D extends DataClass>
}
}
/// Mixin to provide the high-level [limit] methods for users.
mixin LimitContainerMixin<T extends Table, D extends DataClass> on Query<T, D> {
/// Limits the amount of rows returned by capping them at [limit]. If [offset]
/// is provided as well, the first [offset] rows will be skipped and not

View File

@ -0,0 +1,109 @@
part of '../../query_builder.dart';
/// A select statement that is constructed with a raw sql prepared statement
/// instead of the high-level moor api.
class CustomSelectStatement with Selectable<QueryRow> {
/// Tables this select statement reads from. When turning this select query
/// into an auto-updating stream, that stream will emit new items whenever
/// any of these tables changes.
final Set<TableInfo> tables;
/// The sql query string for this statement.
final String query;
/// The variables for the prepared statement, in the order they appear in
/// [query]. Variables are denoted using a question mark in the query.
final List<Variable> variables;
final QueryEngine _db;
/// Constructs a new custom select statement for the query, the variables,
/// the affected tables and the database.
CustomSelectStatement(this.query, this.variables, this.tables, this._db);
/// Constructs a fetcher for this query. The fetcher is responsible for
/// updating a stream at the right moment.
@Deprecated(
'There is no need to use this method. Please use watch() directly')
QueryStreamFetcher<List<QueryRow>> constructFetcher() {
return _constructFetcher();
}
/// Constructs a fetcher for this query. The fetcher is responsible for
/// updating a stream at the right moment.
QueryStreamFetcher<List<QueryRow>> _constructFetcher() {
final args = _mapArgs();
return QueryStreamFetcher<List<QueryRow>>(
readsFrom: tables,
fetchData: () => _executeWithMappedArgs(args),
key: StreamKey(query, args, QueryRow),
);
}
@override
Future<List<QueryRow>> get() async {
return _executeWithMappedArgs(_mapArgs());
}
@override
Stream<List<QueryRow>> watch() {
return _db.createStream(_constructFetcher());
}
/// Executes this query and returns the result.
@Deprecated('Use get() instead')
Future<List<QueryRow>> execute() async {
return get();
}
List<dynamic> _mapArgs() {
final ctx = GenerationContext.fromDb(_db);
return variables.map((v) => v.mapToSimpleValue(ctx)).toList();
}
Future<List<QueryRow>> _executeWithMappedArgs(
List<dynamic> mappedArgs) async {
final result =
await _db.executor.doWhenOpened((e) => e.runSelect(query, mappedArgs));
return result.map((row) => QueryRow(row, _db)).toList();
}
}
/// For custom select statements, represents a row in the result set.
class QueryRow {
/// The raw data in this row.
final Map<String, dynamic> data;
final QueryEngine _db;
/// Construct a row from the raw data and the query engine that maps the raw
/// response to appropriate dart types.
QueryRow(this.data, this._db);
/// Reads an arbitrary value from the row and maps it to a fitting dart type.
/// The dart type [T] must be supported by the type system of the database
/// used (mostly contains booleans, strings, integers and dates).
T read<T>(String key) {
final type = _db.typeSystem.forDartType<T>();
return type.mapFromDatabaseResponse(data[key]);
}
/// Reads a bool from the column named [key].
bool readBool(String key) => read<bool>(key);
/// Reads a string from the column named [key].
String readString(String key) => read<String>(key);
/// Reads a int from the column named [key].
int readInt(String key) => read<int>(key);
/// Reads a double from the column named [key].
double readDouble(String key) => read<double>(key);
/// Reads a [DateTime] from the column named [key].
DateTime readDateTime(String key) => read<DateTime>(key);
/// Reads a [Uint8List] from the column named [key].
Uint8List readBlob(String key) => read<Uint8List>(key);
}

View File

@ -0,0 +1,130 @@
part of '../../query_builder.dart';
/// Signature of a function that generates an [OrderingTerm] when provided with
/// a table.
typedef OrderingTerm OrderClauseGenerator<T>(T tbl);
/// A select statement that doesn't use joins
class SimpleSelectStatement<T extends Table, D extends DataClass>
extends Query<T, D>
with SingleTableQueryMixin<T, D>, LimitContainerMixin<T, D>, Selectable<D> {
/// Whether duplicate rows should be eliminated from the result (this is a
/// `SELECT DISTINCT` statement in sql). Defaults to false.
final bool distinct;
/// Used internally by moor, users will want to call [QueryEngine.select]
/// instead.
SimpleSelectStatement(QueryEngine database, TableInfo<T, D> table,
{this.distinct = false})
: super(database, table);
/// The tables this select statement reads from.
@visibleForOverriding
Set<TableInfo> get watchedTables => {table};
@override
void writeStartPart(GenerationContext ctx) {
ctx.buffer
..write(_beginOfSelect(distinct))
..write(' * FROM ${table.tableWithAlias}');
}
@override
Future<List<D>> get() async {
final ctx = constructQuery();
return _getWithQuery(ctx);
}
Future<List<D>> _getWithQuery(GenerationContext ctx) async {
final results = await ctx.executor.doWhenOpened((e) async {
return await e.runSelect(ctx.sql, ctx.boundVariables);
});
return results.map(table.map).toList();
}
/// Creates a select statement that operates on more than one table by
/// applying the given joins.
///
/// Example from the todolist example which will load the category for each
/// item:
/// ```
/// final results = await select(todos).join([
/// leftOuterJoin(categories, categories.id.equalsExp(todos.category))
/// ]).get();
///
/// return results.map((row) {
/// final entry = row.readTable(todos);
/// final category = row.readTable(categories);
/// return EntryWithCategory(entry, category);
/// }).toList();
/// ```
///
/// See also:
/// - [innerJoin], [leftOuterJoin] and [crossJoin], which can be used to
/// construct a [Join].
/// - [DatabaseConnectionUser.alias], which can be used to build statements
/// that refer to the same table multiple times.
JoinedSelectStatement join(List<Join> joins) {
final statement = JoinedSelectStatement(database, table, joins, distinct);
if (whereExpr != null) {
statement.where(whereExpr.predicate);
}
if (orderByExpr != null) {
statement.orderBy(orderByExpr.terms);
}
return statement;
}
/// Orders the result by the given clauses. The clauses coming first in the
/// list have a higher priority, the later clauses are only considered if the
/// first clause considers two rows to be equal.
///
/// Example that first displays the users who are awesome and sorts users by
/// their id as a secondary criterion:
/// ```
/// (db.select(db.users)
/// ..orderBy([
/// (u) => OrderingTerm(expression: u.isAwesome, mode: OrderingMode.desc),
/// (u) => OrderingTerm(expression: u.id)
/// ]))
/// .get()
/// ```
void orderBy(List<OrderClauseGenerator<T>> clauses) {
orderByExpr = OrderBy(clauses.map((t) => t(table.asDslTable)).toList());
}
@override
Stream<List<D>> watch() {
final query = constructQuery();
final fetcher = QueryStreamFetcher<List<D>>(
readsFrom: watchedTables,
fetchData: () => _getWithQuery(query),
key: StreamKey(query.sql, query.boundVariables, D),
);
return database.createStream(fetcher);
}
}
String _beginOfSelect(bool distinct) {
return distinct ? 'SELECT DISTINCT' : 'SELECT';
}
/// A result row in a [JoinedSelectStatement] that can parse the result of
/// multiple entities.
class TypedResult {
/// Creates the result from the parsed table data.
TypedResult(this._parsedData, this.rawData);
final Map<TableInfo, dynamic> _parsedData;
/// The raw data contained in this row.
final QueryRow rawData;
/// Reads all data that belongs to the given [table] from this row.
D readTable<T extends Table, D extends DataClass>(TableInfo<T, D> table) {
return _parsedData[table] as D;
}
}

View File

@ -0,0 +1,163 @@
part of '../../query_builder.dart';
/// A `SELECT` statement that operates on more than one table.
class JoinedSelectStatement<FirstT extends Table, FirstD extends DataClass>
extends Query<FirstT, FirstD>
with LimitContainerMixin, Selectable<TypedResult> {
/// Whether to generate a `SELECT DISTINCT` query that will remove duplicate
/// rows from the result set.
final bool distinct;
/// Used internally by moor, users should use [SimpleSelectStatement.join]
/// instead.
JoinedSelectStatement(
QueryEngine database, TableInfo<FirstT, FirstD> table, this._joins,
[this.distinct = false])
: super(database, table);
final List<Join> _joins;
/// The tables this select statement reads from
@visibleForOverriding
Set<TableInfo> get watchedTables => _tables.toSet();
// fixed order to make testing easier
Iterable<TableInfo> get _tables =>
<TableInfo>[table].followedBy(_joins.map((j) => j.table));
@override
void writeStartPart(GenerationContext ctx) {
ctx.hasMultipleTables = true;
ctx.buffer..write(_beginOfSelect(distinct))..write(' ');
var isFirst = true;
for (var table in _tables) {
for (var column in table.$columns) {
if (!isFirst) {
ctx.buffer.write(', ');
}
// We run into problems when two tables have a column with the same name
// as we then wouldn't know which column is which. So, we create a
// column alias that matches what is expected by the mapping function
// in _getWithQuery by prefixing the table name.
// We might switch to parsing via the index of the column in a row in
// the future, but that's the solution for now.
column.writeInto(ctx);
ctx.buffer.write(' AS "');
column.writeInto(ctx, ignoreEscape: true);
ctx.buffer.write('"');
isFirst = false;
}
}
ctx.buffer.write(' FROM ${table.tableWithAlias}');
if (_joins.isNotEmpty) {
ctx.writeWhitespace();
for (var i = 0; i < _joins.length; i++) {
if (i != 0) ctx.writeWhitespace();
_joins[i].writeInto(ctx);
}
}
}
/// Applies the [predicate] as the where clause, which will be used to filter
/// results.
///
/// The clause should only refer to columns defined in one of the tables
/// specified during [SimpleSelectStatement.join].
///
/// With the example of a todos table which refers to categories, we can write
/// something like
/// ```dart
/// final query = select(todos)
/// .join([
/// leftOuterJoin(categories, categories.id.equalsExp(todos.category)),
/// ])
/// ..where(and(todos.name.like("%Important"), categories.name.equals("Work")));
/// ```
void where(Expression<bool, BoolType> predicate) {
if (whereExpr == null) {
whereExpr = Where(predicate);
} else {
whereExpr = Where(and(whereExpr.predicate, predicate));
}
}
/// Orders the results of this statement by the ordering [terms].
void orderBy(List<OrderingTerm> terms) {
orderByExpr = OrderBy(terms);
}
@override
Stream<List<TypedResult>> watch() {
final ctx = constructQuery();
final fetcher = QueryStreamFetcher<List<TypedResult>>(
readsFrom: watchedTables,
fetchData: () => _getWithQuery(ctx),
key: StreamKey(ctx.sql, ctx.boundVariables, TypedResult),
);
return database.createStream(fetcher);
}
@override
Future<List<TypedResult>> get() async {
final ctx = constructQuery();
return _getWithQuery(ctx);
}
Future<List<TypedResult>> _getWithQuery(GenerationContext ctx) async {
final results = await ctx.executor.doWhenOpened((e) async {
try {
return await e.runSelect(ctx.sql, ctx.boundVariables);
} catch (e, s) {
final foundTables = <String>{};
for (var table in _tables) {
if (!foundTables.add(table.$tableName)) {
_warnAboutDuplicate(e, s, table);
}
}
rethrow;
}
});
final tables = _tables;
return results.map((row) {
final map = <TableInfo, dynamic>{};
for (var table in tables) {
final prefix = '${table.$tableName}.';
// if all columns of this table are null, skip the table
if (table.$columns.any((c) => row[prefix + c.$name] != null)) {
map[table] = table.map(row, tablePrefix: table.$tableName);
} else {
map[table] = null;
}
}
return TypedResult(map, QueryRow(row, database));
}).toList();
}
@alwaysThrows
void _warnAboutDuplicate(dynamic cause, StackTrace trace, TableInfo table) {
throw MoorWrappedException(
message:
'This query contained the table ${table.actualTableName} more than '
'once. Is this a typo? \n'
'If you need a join that includes the same table more than once, you '
'need to alias() at least one table. See https://moor.simonbinder.eu/queries/joins#aliases '
'for an example.',
cause: cause,
trace: trace,
);
}
}

View File

@ -1,8 +1,4 @@
import 'dart:async';
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';
/// Represents an `UPDATE` statement in sql.
class UpdateStatement<T extends Table, D extends DataClass> extends Query<T, D>

View File

@ -1,126 +0,0 @@
import 'dart:async';
import 'package:meta/meta.dart';
import 'package:moor/moor.dart';
import 'package:moor/src/runtime/components/component.dart';
/// Represents an insert statements
class InsertStatement<D extends DataClass> {
/// The database to use then executing this statement
@protected
final QueryEngine database;
/// The table we're inserting into
@protected
final TableInfo<Table, D> table;
/// Constructs an insert statement from the database and the table. Used
/// internally by moor.
InsertStatement(this.database, this.table);
/// Inserts a row constructed from the fields in [entity].
///
/// 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.
///
/// If [orReplace] is true and a row with the same primary key already exists,
/// the columns of that row will be updated and no new row will be written.
/// Otherwise, an exception will be thrown.
///
/// If the table contains an auto-increment column, the generated value will
/// be returned. If there is no auto-increment column, you can't rely on the
/// return value, but the future will resolve to an error when the insert
/// fails.
Future<int> insert(Insertable<D> entity, {bool orReplace = false}) async {
_validateIntegrity(entity);
final ctx = _createContext(entity, orReplace);
return await database.executor.doWhenOpened((e) async {
final id = await database.executor.runInsert(ctx.sql, ctx.boundVariables);
database.markTablesUpdated({table});
return id;
});
}
/// 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.
/// When a row with the same primary or unique key already exists in the
/// database, the insert will fail. Use [orReplace] to replace rows that
/// already exist.
Future<void> insertAll(List<Insertable<D>> rows,
{bool orReplace = false}) async {
final statements = <String, List<GenerationContext>>{};
// Not every insert has the same sql, as fields which are set to null are
// not included. So, we have a map for sql -> list of variables which we can
// then turn into prepared statements
for (var row in rows) {
_validateIntegrity(row);
final ctx = _createContext(row, orReplace);
statements.putIfAbsent(ctx.sql, () => []).add(ctx);
}
final batchedStatements = statements.entries.map((e) {
final vars = e.value.map((context) => context.boundVariables).toList();
return BatchedStatement(e.key, vars);
}).toList(growable: false);
await database.executor.doWhenOpened((e) async {
await e.runBatched(batchedStatements);
});
database.markTablesUpdated({table});
}
GenerationContext _createContext(Insertable<D> entry, bool replace) {
final map = table.entityToSql(entry.createCompanion(true))
..removeWhere((_, value) => value == null);
final ctx = GenerationContext.fromDb(database);
ctx.buffer
..write('INSERT ')
..write(replace ? 'OR REPLACE ' : '')
..write('INTO ')
..write(table.$tableName)
..write(' ');
if (map.isEmpty) {
ctx.buffer.write('DEFAULT VALUES');
} else {
ctx.buffer
..write('(')
..write(map.keys.join(', '))
..write(') ')
..write('VALUES (');
var first = true;
for (var variable in map.values) {
if (!first) {
ctx.buffer.write(', ');
}
first = false;
variable.writeInto(ctx);
}
ctx.buffer.write(')');
}
return ctx;
}
void _validateIntegrity(Insertable<D> d) {
if (d == null) {
throw InvalidDataException(
'Cannot write null row into ${table.$tableName}');
}
table
.validateIntegrity(d.createCompanion(true), isInserting: true)
.throwIfInvalid(d);
}
}

View File

@ -1,395 +0,0 @@
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/components/join.dart';
import 'package:moor/src/runtime/components/where.dart';
import 'package:moor/src/runtime/database.dart';
import 'package:moor/src/runtime/executor/stream_queries.dart';
import 'package:moor/src/runtime/expressions/expression.dart';
import 'package:moor/src/runtime/statements/query.dart';
import 'package:moor/src/runtime/structure/table_info.dart';
/// Signature of a function that generates an [OrderingTerm] when provided with
/// a table.
typedef OrderingTerm OrderClauseGenerator<T>(T tbl);
/// A `SELECT` statement that operates on more than one table.
class JoinedSelectStatement<FirstT extends Table, FirstD extends DataClass>
extends Query<FirstT, FirstD>
with LimitContainerMixin, Selectable<TypedResult> {
/// Used internally by moor, users should use [SimpleSelectStatement.join]
/// instead.
JoinedSelectStatement(
QueryEngine database, TableInfo<FirstT, FirstD> table, this._joins)
: super(database, table);
final List<Join> _joins;
/// The tables this select statement reads from
@visibleForOverriding
Set<TableInfo> get watchedTables => _tables.toSet();
// fixed order to make testing easier
Iterable<TableInfo> get _tables =>
<TableInfo>[table].followedBy(_joins.map((j) => j.table));
@override
void writeStartPart(GenerationContext ctx) {
ctx.hasMultipleTables = true;
ctx.buffer.write('SELECT ');
var isFirst = true;
for (var table in _tables) {
for (var column in table.$columns) {
if (!isFirst) {
ctx.buffer.write(', ');
}
// We run into problems when two tables have a column with the same name
// as we then wouldn't know which column is which. So, we create a
// column alias that matches what is expected by the mapping function
// in _getWithQuery by prefixing the table name.
// We might switch to parsing via the index of the column in a row in
// the future, but that's the solution for now.
column.writeInto(ctx);
ctx.buffer.write(' AS "');
column.writeInto(ctx, ignoreEscape: true);
ctx.buffer.write('"');
isFirst = false;
}
}
ctx.buffer.write(' FROM ${table.tableWithAlias}');
if (_joins.isNotEmpty) {
ctx.writeWhitespace();
for (var i = 0; i < _joins.length; i++) {
if (i != 0) ctx.writeWhitespace();
_joins[i].writeInto(ctx);
}
}
}
/// Applies the [predicate] as the where clause, which will be used to filter
/// results.
///
/// The clause should only refer to columns defined in one of the tables
/// specified during [SimpleSelectStatement.join].
///
/// With the example of a todos table which refers to categories, we can write
/// something like
/// ```dart
/// final query = select(todos)
/// .join([
/// leftOuterJoin(categories, categories.id.equalsExp(todos.category)),
/// ])
/// ..where(and(todos.name.like("%Important"), categories.name.equals("Work")));
/// ```
void where(Expression<bool, BoolType> predicate) {
if (whereExpr == null) {
whereExpr = Where(predicate);
} else {
whereExpr = Where(and(whereExpr.predicate, predicate));
}
}
/// Orders the results of this statement by the ordering [terms].
void orderBy(List<OrderingTerm> terms) {
orderByExpr = OrderBy(terms);
}
@override
Stream<List<TypedResult>> watch() {
final ctx = constructQuery();
final fetcher = QueryStreamFetcher<List<TypedResult>>(
readsFrom: watchedTables,
fetchData: () => _getWithQuery(ctx),
key: StreamKey(ctx.sql, ctx.boundVariables, TypedResult),
);
return database.createStream(fetcher);
}
@override
Future<List<TypedResult>> get() async {
final ctx = constructQuery();
return _getWithQuery(ctx);
}
Future<List<TypedResult>> _getWithQuery(GenerationContext ctx) async {
final results = await ctx.executor.doWhenOpened((e) async {
try {
return await e.runSelect(ctx.sql, ctx.boundVariables);
} catch (e, s) {
final foundTables = <String>{};
for (var table in _tables) {
if (!foundTables.add(table.$tableName)) {
_warnAboutDuplicate(e, s, table);
}
}
rethrow;
}
});
final tables = _tables;
return results.map((row) {
final map = <TableInfo, dynamic>{};
for (var table in tables) {
final prefix = '${table.$tableName}.';
// if all columns of this table are null, skip the table
if (table.$columns.any((c) => row[prefix + c.$name] != null)) {
map[table] = table.map(row, tablePrefix: table.$tableName);
} else {
map[table] = null;
}
}
return TypedResult(map, QueryRow(row, database));
}).toList();
}
@alwaysThrows
void _warnAboutDuplicate(dynamic cause, StackTrace trace, TableInfo table) {
throw MoorWrappedException(
message:
'This query contained the table ${table.actualTableName} more than '
'once. Is this a typo? \n'
'If you need a join that includes the same table more than once, you '
'need to alias() at least one table. See https://moor.simonbinder.eu/queries/joins#aliases '
'for an example.',
cause: cause,
trace: trace,
);
}
}
/// A select statement that doesn't use joins
class SimpleSelectStatement<T extends Table, D extends DataClass>
extends Query<T, D>
with SingleTableQueryMixin<T, D>, LimitContainerMixin<T, D>, Selectable<D> {
/// Used internally by moor, users will want to call [QueryEngine.select]
/// instead.
SimpleSelectStatement(QueryEngine database, TableInfo<T, D> table)
: super(database, table);
/// The tables this select statement reads from.
@visibleForOverriding
Set<TableInfo> get watchedTables => {table};
@override
void writeStartPart(GenerationContext ctx) {
ctx.buffer.write('SELECT * FROM ${table.tableWithAlias}');
}
@override
Future<List<D>> get() async {
final ctx = constructQuery();
return _getWithQuery(ctx);
}
Future<List<D>> _getWithQuery(GenerationContext ctx) async {
final results = await ctx.executor.doWhenOpened((e) async {
return await e.runSelect(ctx.sql, ctx.boundVariables);
});
return results.map(table.map).toList();
}
/// Creates a select statement that operates on more than one table by
/// applying the given joins.
///
/// Example from the todolist example which will load the category for each
/// item:
/// ```
/// final results = await select(todos).join([
/// leftOuterJoin(categories, categories.id.equalsExp(todos.category))
/// ]).get();
///
/// return results.map((row) {
/// final entry = row.readTable(todos);
/// final category = row.readTable(categories);
/// return EntryWithCategory(entry, category);
/// }).toList();
/// ```
///
/// See also:
/// - [innerJoin], [leftOuterJoin] and [crossJoin], which can be used to
/// construct a [Join].
/// - [DatabaseConnectionUser.alias], which can be used to build statements
/// that refer to the same table multiple times.
JoinedSelectStatement join(List<Join> joins) {
final statement = JoinedSelectStatement(database, table, joins);
if (whereExpr != null) {
statement.where(whereExpr.predicate);
}
if (orderByExpr != null) {
statement.orderBy(orderByExpr.terms);
}
return statement;
}
/// Orders the result by the given clauses. The clauses coming first in the
/// list have a higher priority, the later clauses are only considered if the
/// first clause considers two rows to be equal.
///
/// Example that first displays the users who are awesome and sorts users by
/// their id as a secondary criterion:
/// ```
/// (db.select(db.users)
/// ..orderBy([
/// (u) => OrderingTerm(expression: u.isAwesome, mode: OrderingMode.desc),
/// (u) => OrderingTerm(expression: u.id)
/// ]))
/// .get()
/// ```
void orderBy(List<OrderClauseGenerator<T>> clauses) {
orderByExpr = OrderBy(clauses.map((t) => t(table.asDslTable)).toList());
}
@override
Stream<List<D>> watch() {
final query = constructQuery();
final fetcher = QueryStreamFetcher<List<D>>(
readsFrom: watchedTables,
fetchData: () => _getWithQuery(query),
key: StreamKey(query.sql, query.boundVariables, D),
);
return database.createStream(fetcher);
}
}
/// A select statement that is constructed with a raw sql prepared statement
/// instead of the high-level moor api.
class CustomSelectStatement with Selectable<QueryRow> {
/// Tables this select statement reads from. When turning this select query
/// into an auto-updating stream, that stream will emit new items whenever
/// any of these tables changes.
final Set<TableInfo> tables;
/// The sql query string for this statement.
final String query;
/// The variables for the prepared statement, in the order they appear in
/// [query]. Variables are denoted using a question mark in the query.
final List<Variable> variables;
final QueryEngine _db;
/// Constructs a new custom select statement for the query, the variables,
/// the affected tables and the database.
CustomSelectStatement(this.query, this.variables, this.tables, this._db);
/// Constructs a fetcher for this query. The fetcher is responsible for
/// updating a stream at the right moment.
@Deprecated(
'There is no need to use this method. Please use watch() directly')
QueryStreamFetcher<List<QueryRow>> constructFetcher() {
return _constructFetcher();
}
/// Constructs a fetcher for this query. The fetcher is responsible for
/// updating a stream at the right moment.
QueryStreamFetcher<List<QueryRow>> _constructFetcher() {
final args = _mapArgs();
return QueryStreamFetcher<List<QueryRow>>(
readsFrom: tables,
fetchData: () => _executeWithMappedArgs(args),
key: StreamKey(query, args, QueryRow),
);
}
@override
Future<List<QueryRow>> get() async {
return _executeWithMappedArgs(_mapArgs());
}
@override
Stream<List<QueryRow>> watch() {
return _db.createStream(_constructFetcher());
}
/// Executes this query and returns the result.
@Deprecated('Use get() instead')
Future<List<QueryRow>> execute() async {
return get();
}
List<dynamic> _mapArgs() {
final ctx = GenerationContext.fromDb(_db);
return variables.map((v) => v.mapToSimpleValue(ctx)).toList();
}
Future<List<QueryRow>> _executeWithMappedArgs(
List<dynamic> mappedArgs) async {
final result =
await _db.executor.doWhenOpened((e) => e.runSelect(query, mappedArgs));
return result.map((row) => QueryRow(row, _db)).toList();
}
}
/// A result row in a [JoinedSelectStatement] that can parse the result of
/// multiple entities.
class TypedResult {
/// Creates the result from the parsed table data.
TypedResult(this._parsedData, this.rawData);
final Map<TableInfo, dynamic> _parsedData;
/// The raw data contained in this row.
final QueryRow rawData;
/// Reads all data that belongs to the given [table] from this row.
D readTable<T extends Table, D extends DataClass>(TableInfo<T, D> table) {
return _parsedData[table] as D;
}
}
/// For custom select statements, represents a row in the result set.
class QueryRow {
/// The raw data in this row.
final Map<String, dynamic> data;
final QueryEngine _db;
/// Construct a row from the raw data and the query engine that maps the raw
/// response to appropriate dart types.
QueryRow(this.data, this._db);
/// Reads an arbitrary value from the row and maps it to a fitting dart type.
/// The dart type [T] must be supported by the type system of the database
/// used (mostly contains booleans, strings, integers and dates).
T read<T>(String key) {
final type = _db.typeSystem.forDartType<T>();
return type.mapFromDatabaseResponse(data[key]);
}
/// Reads a bool from the column named [key].
bool readBool(String key) => read<bool>(key);
/// Reads a string from the column named [key].
String readString(String key) => read<String>(key);
/// Reads a int from the column named [key].
int readInt(String key) => read<int>(key);
/// Reads a double from the column named [key].
double readDouble(String key) => read<double>(key);
/// Reads a [DateTime] from the column named [key].
DateTime readDateTime(String key) => read<DateTime>(key);
/// Reads a [Uint8List] from the column named [key].
Uint8List readBlob(String key) => read<Uint8List>(key);
}

View File

@ -3,6 +3,7 @@ import 'dart:typed_data';
import 'package:convert/convert.dart';
part 'custom_type.dart';
part 'type_system.dart';
/// A type that can be mapped from Dart to sql. The generic type parameter here
/// denotes the resolved dart type.

View File

@ -1,4 +1,4 @@
import 'package:moor/src/types/sql_types.dart';
part of 'sql_types.dart';
/// Manages the set of [SqlType] known to a database. It's also responsible for
/// returning the appropriate sql type for a given dart type.

View File

@ -1,5 +1,4 @@
import 'package:moor/moor.dart';
import 'package:moor/src/runtime/components/component.dart';
import 'package:test/test.dart';
void main() {

View File

@ -1,4 +1,3 @@
import 'package:moor/src/runtime/components/component.dart';
import 'package:test/test.dart';
import 'package:moor/moor.dart';

View File

@ -73,4 +73,10 @@ void main() {
// shouldn't call stream queries - we didn't set the updates parameter
verifyNever(streamQueries.handleTableUpdates(any));
});
test('custom statement', () async {
// regression test for https://github.com/simolus3/moor/issues/199 - the
// mock will throw when used before opening
expect(db.customStatement('UPDATE tbl SET a = b'), completes);
});
}

View File

@ -926,6 +926,22 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
readsFrom: {config}).map(_rowToConfig);
}
ReadRowIdResult _rowToReadRowIdResult(QueryRow row) {
return ReadRowIdResult(
rowid: row.readInt('rowid'),
configKey: row.readString('config_key'),
configValue: row.readString('config_value'),
);
}
Selectable<ReadRowIdResult> readRowId(Expression<int, IntType> expr) {
final generatedexpr = $write(expr);
return customSelectQuery(
'SELECT oid, * FROM config WHERE _rowid_ = ${generatedexpr.sql}',
variables: [...generatedexpr.introducedVariables],
readsFrom: {config}).map(_rowToReadRowIdResult);
}
Future<int> writeConfig(String key, String value) {
return customInsert(
'REPLACE INTO config VALUES (:key, :value)',
@ -938,3 +954,24 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
List<TableInfo> get allTables =>
[noIds, withDefaults, withConstraints, config, mytable];
}
class ReadRowIdResult {
final int rowid;
final String configKey;
final String configValue;
ReadRowIdResult({
this.rowid,
this.configKey,
this.configValue,
});
@override
int get hashCode => $mrjf(
$mrjc(rowid.hashCode, $mrjc(configKey.hashCode, configValue.hashCode)));
@override
bool operator ==(other) =>
identical(this, other) ||
(other is ReadRowIdResult &&
other.rowid == this.rowid &&
other.configKey == this.configKey &&
other.configValue == this.configValue);
}

View File

@ -29,4 +29,6 @@ CREATE TABLE mytable (
readConfig: SELECT * FROM config WHERE config_key = ?;
readMultiple: SELECT * FROM config WHERE config_key IN ? ORDER BY $clause;
readDynamic: SELECT * FROM config WHERE $predicate;
readDynamic: SELECT * FROM config WHERE $predicate;
readRowId: SELECT oid, * FROM config WHERE _rowid_ = $expr;

View File

@ -29,6 +29,10 @@ class MockExecutor extends Mock implements QueryExecutor {
assert(_opened);
return Future.value(0);
});
when(runCustom(any, any)).thenAnswer((_) {
assert(_opened);
return Future.value(0);
});
when(beginTransaction()).thenAnswer((_) {
assert(_opened);
return transactions;

View File

@ -1,5 +1,4 @@
import 'package:moor/moor.dart';
import 'package:moor/src/runtime/components/component.dart';
import 'package:test/test.dart';
import '../data/tables/todos.dart';

View File

@ -1,5 +1,4 @@
import 'package:moor/moor.dart';
import 'package:moor/src/runtime/components/component.dart';
import 'package:test/test.dart';
import '../data/tables/todos.dart';

View File

@ -1,6 +1,4 @@
import 'package:moor/moor.dart';
import 'package:moor/src/runtime/components/component.dart';
import 'package:moor/src/runtime/expressions/expression.dart';
import 'package:test/test.dart';
typedef Expression<int, IntType> _Extractor(

View File

@ -1,12 +1,12 @@
import 'package:moor/src/runtime/components/component.dart';
import 'package:test/test.dart';
import 'package:moor/moor.dart';
import 'package:moor/moor.dart' as moor;
import '../data/tables/todos.dart';
void main() {
test('in expressions are generated', () {
final innerExpression = moor.GeneratedTextColumn('name', null, true);
final innerExpression = GeneratedTextColumn('name', null, true);
final isInExpression = moor.isIn(innerExpression, ['Max', 'Tobias']);
final context = GenerationContext.fromDb(TodoDb(null));

View File

@ -1,26 +1,26 @@
import 'package:moor/src/runtime/components/component.dart';
import 'package:test/test.dart';
import 'package:moor/moor.dart';
import 'package:moor/moor.dart' as moor;
import 'package:test/test.dart';
import '../data/tables/todos.dart';
void main() {
final innerExpression = moor.GeneratedTextColumn('name', null, true);
final innerExpression = GeneratedTextColumn('name', null, true);
test('IS NULL expressions are generated', () {
final isNull = moor.isNull(innerExpression);
final expr = moor.isNull(innerExpression);
final context = GenerationContext.fromDb(TodoDb(null));
isNull.writeInto(context);
expr.writeInto(context);
expect(context.sql, 'name IS NULL');
});
test('IS NOT NULL expressions are generated', () {
final isNotNull = moor.isNotNull(innerExpression);
final expr = moor.isNotNull(innerExpression);
final context = GenerationContext.fromDb(TodoDb(null));
isNotNull.writeInto(context);
expr.writeInto(context);
expect(context.sql, 'name IS NOT NULL');
});

View File

@ -1,5 +1,4 @@
import 'package:moor/moor.dart';
import 'package:moor/src/runtime/components/component.dart';
import 'package:test/test.dart';
import '../data/tables/todos.dart';

View File

@ -1,4 +1,3 @@
import 'package:moor/src/runtime/components/component.dart';
import 'package:test/test.dart';
import 'package:moor/moor.dart';

View File

@ -37,6 +37,20 @@ void main() {
});
test('generates insert or replace statements', () async {
await db.into(db.todosTable).insert(
TodoEntry(
id: 113,
content: 'Done',
),
mode: InsertMode.insertOrReplace);
verify(executor.runInsert(
'INSERT OR REPLACE INTO todos (id, content) VALUES (?, ?)',
[113, 'Done']));
});
test('generates insert or replace statements with legacy parameter',
() async {
await db.into(db.todosTable).insert(
TodoEntry(
id: 113,

View File

@ -1,5 +1,4 @@
import 'package:moor/moor.dart';
import 'package:moor/src/runtime/components/join.dart';
import 'package:test/test.dart';
import 'data/tables/todos.dart';
import 'data/utils/mocks.dart';
@ -48,7 +47,7 @@ void main() {
]);
});
final result = await db.select(todos).join([
final result = await db.select(todos, distinct: true).join([
leftOuterJoin(categories, categories.id.equalsExp(todos.category))
]).get();
@ -67,6 +66,8 @@ void main() {
expect(
row.readTable(categories), Category(id: 3, description: 'description'));
verify(executor.runSelect(argThat(contains('DISTINCT')), any));
});
test('reports null when no data is available', () async {

View File

@ -31,8 +31,9 @@ void main() {
group('SELECT statements are generated', () {
test('for simple statements', () {
db.select(db.users).get();
verify(executor.runSelect('SELECT * FROM users;', argThat(isEmpty)));
db.select(db.users, distinct: true).get();
verify(executor.runSelect(
'SELECT DISTINCT * FROM users;', argThat(isEmpty)));
});
test('with limit statements', () {

View File

@ -67,7 +67,8 @@ class QueryHandler {
final tableFinder = ReferencedTablesVisitor();
_select.accept(tableFinder);
_foundTables = tableFinder.foundTables;
final moorTables = _foundTables.map(mapper.tableToMoor).toList();
final moorTables =
_foundTables.map(mapper.tableToMoor).where((s) => s != null).toList();
return SqlSelectQuery(
name, context, _foundElements, moorTables, _inferResultSet());
@ -100,6 +101,12 @@ class QueryHandler {
final table = candidatesForSingleTable.single;
final moorTable = mapper.tableToMoor(table);
if (moorTable == null) {
// References a table not declared in any moor api (dart or moor file).
// This can happen for internal sqlite tables
return InferredResultSet(null, columns);
}
final resultEntryToColumn = <ResultColumn, String>{};
var matches = true;
@ -124,7 +131,7 @@ class QueryHandler {
// we have established that all columns in resultEntryToColumn do appear
// in the moor table. Now check for set equality.
if (resultEntryToColumn.length != moorTable.columns.length) {
if (rawColumns.length != moorTable.columns.length) {
matches = false;
}

View File

@ -122,8 +122,7 @@ class MoorDriver implements AnalysisDriverGeneric {
final factory = dartDriver.sourceFactory;
final baseSource = base == null ? null : factory.forUri2(base);
final source =
dartDriver.sourceFactory.resolveUri(baseSource, reference.toString());
final source = factory.resolveUri(baseSource, reference.toString());
return source.fullName;
}

View File

@ -61,17 +61,9 @@ class _NavigationVisitor extends RecursiveVisitor<void> {
final resolved = e.resolved;
if (resolved is Column) {
// if we know the declaration because the file was analyzed - use that
final declaration = resolved.meta<ColumnDeclaration>();
if (declaration != null) {
final location = locationOfDeclaration(declaration);
_reportForSpan(e.span, ElementKind.FIELD, location);
} else if (declaration is ExpressionColumn) {
// expression references don't have an explicit declaration, but they
// reference an expression that we can target
final expr = (declaration as ExpressionColumn).expression;
final target = locationOfNode(request.file, expr);
_reportForSpan(e.span, ElementKind.LOCAL_VARIABLE, target);
final locations = _locationOfColumn(resolved);
for (final declaration in locations) {
_reportForSpan(e.span, ElementKind.FIELD, declaration);
}
}
}
@ -79,6 +71,29 @@ class _NavigationVisitor extends RecursiveVisitor<void> {
visitChildren(e);
}
Iterable<Location> _locationOfColumn(Column column) sync* {
final declaration = column.meta<ColumnDeclaration>();
if (declaration != null) {
// the column was declared in a table and we happen to know where the
// declaration is - point to that declaration.
final location = locationOfDeclaration(declaration);
yield location;
} else if (column is ExpressionColumn) {
// expression references don't have an explicit declaration, but they
// reference an expression that we can target
final expr = (declaration as ExpressionColumn).expression;
yield locationOfNode(request.file, expr);
} else if (column is CompoundSelectColumn) {
// a compound select column consists of multiple column declarations -
// let's use all of them
yield* column.columns.where((c) => c != null).expand(_locationOfColumn);
} else if (column is DelegatedColumn) {
if (column.innerColumn != null) {
yield* _locationOfColumn(column.innerColumn);
}
}
}
@override
void visitQueryable(Queryable e) {
if (e is TableReference) {

View File

@ -12,7 +12,9 @@ environment:
sdk: '>=2.2.0 <3.0.0'
dependencies:
analyzer: '>=0.36.4 <0.39.0'
# todo it looks like we're not using any apis removed in 0.40.0, but I couldn't verify that (neither analyzer_plugin
# nor build supports 0.40.0 yet)
analyzer: '>=0.36.4 <0.40.0'
analyzer_plugin: '>=0.1.0 <0.3.0'
collection: ^1.14.0
recase: ^2.0.1

View File

@ -3,10 +3,13 @@
[build]
base = "docs"
publish = "docs/public"
command = 'git submodule update --init --recursive && HUGO_ENV="production" hugo'
command = 'git submodule update --init --recursive && hugo'
[[redirects]]
from = "https://moor.netlify.com/*"
to = "https://moor.simonbinder.eu/:splat"
status = 301
force = true
force = true
[context.production]
environment = { HUGO_ENV="production" }

View File

@ -1,3 +1,8 @@
## unreleased
- Support common table expressions
- Handle special `rowid`, `oid`, `__rowid__` references
- Support references to `sqlite_master` and `sqlite_sequence` tables
## 0.3.0
- parse compound select statements
- scan comment tokens

View File

@ -60,5 +60,6 @@ enum AnalysisErrorType {
unknownFunction,
compoundColumnCountMismatch,
cteColumnCountMismatch,
other,
}

View File

@ -30,6 +30,53 @@ class TableColumn extends Column {
Table table;
TableColumn(this.name, this.type, {this.definition});
/// Whether this column is an alias for the rowid, as defined in
/// https://www.sqlite.org/lang_createtable.html#rowid
///
/// To summarize, a column is an alias for the rowid if all of the following
/// conditions are met:
/// - the table has a primary key that consists of exactly one (this) column
/// - the column is declared to be an integer
/// - if this column has a [PrimaryKeyColumn], the [OrderingMode] of that
/// constraint is not [OrderingMode.descending].
bool isAliasForRowId() {
if (definition == null ||
table == null ||
type?.type != BasicType.int ||
table.withoutRowId) {
return false;
}
// We need to check whether this column is a primary key, which could happen
// because of a table or a column constraint
for (var tableConstraint in table.tableConstraints.whereType<KeyClause>()) {
if (!tableConstraint.isPrimaryKey) continue;
final columns = tableConstraint.indexedColumns;
if (columns.length == 1 && columns.single.columnName == name) {
return true;
}
}
// option 2: This column has a primary key constraint
for (var primaryConstraint in constraints.whereType<PrimaryKeyColumn>()) {
if (primaryConstraint.mode == OrderingMode.descending) return false;
// additional restriction: Column type must be exactly "INTEGER"
return definition.typeName == 'INTEGER';
}
return false;
}
}
/// Refers to the special "rowid", "oid" or "_rowid_" column defined for tables
/// that weren't created with an `WITHOUT ROWID` clause.
class RowId extends TableColumn {
// note that such alias is always called "rowid" in the result set -
// "SELECT oid FROM table" yields a sinle column called "rowid"
RowId() : super('rowid', const ResolvedType(type: BasicType.int));
}
/// A column that is created by an expression. For instance, in the select
@ -44,8 +91,31 @@ class ExpressionColumn extends Column {
ExpressionColumn({@required this.name, this.expression});
}
/// A column that is created by a reference expression. The difference to an
/// [ExpressionColumn] is that the correct case of the column name depends on
/// the resolved reference.
class ReferenceExpressionColumn extends ExpressionColumn {
Reference get reference => expression as Reference;
@override
String get name => overriddenName ?? reference.resolvedColumn?.name;
final String overriddenName;
ReferenceExpressionColumn(Reference ref, {this.overriddenName})
: super(name: null, expression: ref);
}
/// A column that wraps another column.
mixin DelegatedColumn on Column {
Column get innerColumn;
@override
String get name => innerColumn.name;
}
/// The result column of a [CompoundSelectStatement].
class CompoundSelectColumn extends Column {
class CompoundSelectColumn extends Column with DelegatedColumn {
/// The column in [CompoundSelectStatement.base] each of the
/// [CompoundSelectStatement.additional] that contributed to this column.
final List<Column> columns;
@ -53,5 +123,18 @@ class CompoundSelectColumn extends Column {
CompoundSelectColumn(this.columns);
@override
String get name => columns.first.name;
Column get innerColumn => columns.first;
}
class CommonTableExpressionColumn extends Column with DelegatedColumn {
@override
final String name;
@override
Column innerColumn;
// note that innerColumn is mutable because the column might not be known
// during all analysis phases.
CommonTableExpressionColumn(this.name, this.innerColumn);
}

View File

@ -44,6 +44,10 @@ class ReferenceScope {
return ReferenceScope(this, root: effectiveRoot);
}
ReferenceScope createSibling() {
return parent.createChild();
}
/// Registers something that can be referenced in this and child scopes.
void register(String identifier, Referencable ref) {
_references.putIfAbsent(identifier.toUpperCase(), () => []).add(ref);

View File

@ -1,5 +1,9 @@
part of '../analysis.dart';
/// The aliases which can be used to refer to the rowid of a table. See
/// https://www.sqlite.org/lang_createtable.html#rowid
const aliasesForRowId = ['rowid', 'oid', '_rowid_'];
/// Something that will resolve to an [ResultSet] when referred to via
/// the [ReferenceScope].
abstract class ResolvesToResultSet with Referencable {
@ -39,6 +43,8 @@ class Table with ResultSet, VisibleToChildren, HasMetaMixin {
/// The ast node that created this table
final CreateTableStatement definition;
TableColumn _rowIdColumn;
/// Constructs a table from the known [name] and [resolvedColumns].
Table(
{@required this.name,
@ -48,6 +54,23 @@ class Table with ResultSet, VisibleToChildren, HasMetaMixin {
this.definition}) {
for (var column in resolvedColumns) {
column.table = this;
if (_rowIdColumn == null && column.isAliasForRowId()) {
_rowIdColumn = column;
}
}
}
@override
Column findColumn(String name) {
final defaultSearch = super.findColumn(name);
if (defaultSearch != null) return defaultSearch;
// handle aliases to rowids, see https://www.sqlite.org/lang_createtable.html#rowid
if (aliasesForRowId.contains(name.toLowerCase()) && !withoutRowId) {
return _rowIdColumn ?? RowId()
..table = this;
}
return null;
}
}

View File

@ -11,8 +11,9 @@ class ColumnResolver extends RecursiveVisitor<void> {
@override
void visitSelectStatement(SelectStatement e) {
_resolveSelect(e);
// visit children first so that common table expressions are resolved
visitChildren(e);
_resolveSelect(e);
}
@override
@ -53,6 +54,22 @@ class ColumnResolver extends RecursiveVisitor<void> {
e.resolvedColumns = resolved;
}
@override
void visitCommonTableExpression(CommonTableExpression e) {
visitChildren(e);
final resolved = e.as.resolvedColumns;
final names = e.columnNames;
if (names != null && resolved != null && names.length != resolved.length) {
context.reportError(AnalysisError(
type: AnalysisErrorType.cteColumnCountMismatch,
message: 'This CTE declares ${names.length} columns, but its select '
'statement actually returns ${resolved.length}.',
relevantNode: e,
));
}
}
@override
void visitUpdateStatement(UpdateStatement e) {
final table = _resolveTableReference(e.table);
@ -78,8 +95,12 @@ class ColumnResolver extends RecursiveVisitor<void> {
void _handle(Queryable queryable, List<Column> availableColumns) {
queryable.when(
isTable: (table) {
_resolveTableReference(table);
availableColumns.addAll(table.resultSet.resolvedColumns);
final resolved = _resolveTableReference(table);
if (resolved != null) {
// an error will be logged when resolved is null, so the != null check
// is fine and avoids crashes
availableColumns.addAll(table.resultSet.resolvedColumns);
}
},
isSelect: (select) {
// the inner select statement doesn't have access to columns defined in
@ -126,15 +147,27 @@ class ColumnResolver extends RecursiveVisitor<void> {
usedColumns.addAll(availableColumns);
}
} else if (resultColumn is ExpressionResultColumn) {
final name = _nameOfResultColumn(resultColumn);
final column =
ExpressionColumn(name: name, expression: resultColumn.expression);
final expression = resultColumn.expression;
Column column;
if (expression is Reference) {
column = ReferenceExpressionColumn(expression,
overriddenName: resultColumn.as);
} else {
final name = _nameOfResultColumn(resultColumn);
column =
ExpressionColumn(name: name, expression: resultColumn.expression);
}
usedColumns.add(column);
// make this column available if there is no other with the same name
if (!availableColumns.any((c) => c.name == name)) {
availableColumns.add(column);
if (resultColumn.as != null) {
// make this column available for references if there is no other
// column with the same name
final name = resultColumn.as;
if (!availableColumns.any((c) => c.name == name)) {
availableColumns.add(column);
}
}
}
}
@ -158,9 +191,9 @@ class ColumnResolver extends RecursiveVisitor<void> {
return span;
}
Table _resolveTableReference(TableReference r) {
ResultSet _resolveTableReference(TableReference r) {
final scope = r.scope;
final resolvedTable = scope.resolve<Table>(r.tableName, orElse: () {
final resolvedTable = scope.resolve<ResultSet>(r.tableName, orElse: () {
final available = scope.allOf<Table>().map((t) => t.name);
context.reportError(UnresolvedReferenceError(

View File

@ -38,7 +38,9 @@ class AstPreparingVisitor extends RecursiveVisitor<void> {
final scope = e.scope;
if (isInFROM) {
final forked = scope.effectiveRoot.createChild();
final surroundingSelect =
e.parents.firstWhere((node) => node is BaseSelectStatement).scope;
final forked = surroundingSelect.createSibling();
e.scope = forked;
} else {
final forked = scope.createChild();
@ -125,6 +127,12 @@ class AstPreparingVisitor extends RecursiveVisitor<void> {
visitChildren(e);
}
@override
void visitCommonTableExpression(CommonTableExpression e) {
e.scope.register(e.cteTableName, e);
visitChildren(e);
}
@override
void visitNumberedVariable(NumberedVariable e) {
_foundVariables.add(e);
@ -170,4 +178,18 @@ class AstPreparingVisitor extends RecursiveVisitor<void> {
}
}
}
void _forkScope(AstNode node) {
node.scope = node.scope.createChild();
}
@override
void visitChildren(AstNode e) {
// hack to fork scopes on statements (selects are handled above)
if (e is Statement && e is! SelectStatement) {
_forkScope(e);
}
super.visitChildren(e);
}
}

View File

@ -42,6 +42,16 @@ class ReferenceResolver extends RecursiveVisitor<void> {
e.resolved = column;
}
}
} else if (aliasesForRowId.contains(e.columnName.toLowerCase())) {
// special case for aliases to a rowid
final column = _resolveRowIdAlias(e);
if (column == null) {
context.reportError(AnalysisError(
type: AnalysisErrorType.referencedUnknownColumn, relevantNode: e));
} else {
e.resolved = column;
}
} else {
// find any column with the referenced name.
// todo special case for USING (...) in joins?
@ -67,6 +77,24 @@ class ReferenceResolver extends RecursiveVisitor<void> {
visitChildren(e);
}
Column _resolveRowIdAlias(Reference e) {
// to resolve those aliases when they're not bound to a table, the
// surrounding select statement may only read from one table
final select = e.parents.firstWhere((node) => node is SelectStatement,
orElse: () => null) as SelectStatement;
if (select == null) return null;
if (select.from.length != 1 || select.from.single is! TableReference) {
return null;
}
final table = (select.from.single as TableReference).resolved as Table;
if (table == null) return null;
// table.findColumn contains logic to resolve row id aliases
return table.findColumn(e.columnName);
}
@override
void visitAggregateExpression(AggregateExpression e) {
if (e.windowName != null && e.resolved == null) {

View File

@ -12,7 +12,7 @@ class TypeResolvingVisitor extends RecursiveVisitor<void> {
@override
void visitChildren(AstNode e) {
// called for every ast node, so we implement this here
if (e is Expression) {
if (e is Expression && !types.needsToBeInferred(e)) {
types.resolveExpression(e);
} else if (e is SelectStatement) {
e.resolvedColumns.forEach(types.resolveColumn);
@ -41,8 +41,12 @@ class TypeResolvingVisitor extends RecursiveVisitor<void> {
}
}
}
}
visitChildren(e);
// we already handled the source tuples, don't visit them
visitChildren(e.table);
e.targetColumns.forEach(visitChildren);
} else {
visitChildren(e);
}
}
}

View File

@ -49,10 +49,14 @@ class TypeResolver {
return !containsVariable;
}
bool needsToBeInferred(Typeable t) {
return t is Variable || t is DartExpressionPlaceholder;
}
ResolveResult resolveOrInfer(Typeable t) {
if (t is Column) {
return resolveColumn(t);
} else if (t is Variable || t is DartExpressionPlaceholder) {
} else if (needsToBeInferred(t)) {
return inferType(t as Expression);
} else if (t is Expression) {
return resolveExpression(t);
@ -78,9 +82,8 @@ class TypeResolver {
return ResolveResult(column.type);
} else if (column is ExpressionColumn) {
return resolveOrInfer(column.expression);
} else if (column is CompoundSelectColumn) {
// todo maybe use a type that matches every column in here?
return resolveColumn(column.columns.first);
} else if (column is DelegatedColumn) {
return resolveColumn(column.innerColumn);
}
throw StateError('Unknown column $column');
@ -95,7 +98,8 @@ class TypeResolver {
return resolveExpression(expr.inner);
} else if (expr is Parentheses) {
return resolveExpression(expr.expression);
} else if (expr is Variable) {
} else if (expr is Variable || expr is Tuple) {
// todo we can probably resolve tuples by looking at their content
return const ResolveResult.needsContext();
} else if (expr is Reference) {
return resolveColumn(expr.resolved as Column);
@ -169,7 +173,8 @@ class TypeResolver {
ResolveResult resolveFunctionCall(Invocation call) {
return _cache((Invocation call) {
final parameters = _expandParameters(call);
final firstNullable = justResolve(parameters.first).nullable;
final firstNullable =
parameters.isEmpty ? false : justResolve(parameters.first).nullable;
final anyNullable = parameters.map(justResolve).any((r) => r.nullable);
switch (call.name.toLowerCase()) {

View File

@ -7,6 +7,7 @@ import 'package:sqlparser/src/utils/meta.dart';
part 'clauses/limit.dart';
part 'clauses/ordering.dart';
part 'clauses/with.dart';
part 'common/queryables.dart';
part 'common/renamable.dart';
@ -51,6 +52,10 @@ abstract class AstNode with HasMetaMixin {
/// all nodes.
Token last;
/// Whether this ast node is synthetic, meaning that it doesn't appear in the
/// actual source.
bool synthetic;
/// The first index in the source that belongs to this node. Not set for all
/// nodes.
int get firstPosition => first.span.start.offset;
@ -137,6 +142,14 @@ abstract class AstNode with HasMetaMixin {
/// type. The "content" refers to anything stored only in this node, children
/// are ignored.
bool contentEquals(covariant AstNode other);
@override
String toString() {
if (hasSpan) {
return '$runtimeType: ${span.text}';
}
return super.toString();
}
}
abstract class AstVisitor<T> {
@ -149,6 +162,8 @@ abstract class AstVisitor<T> {
T visitUpdateStatement(UpdateStatement e);
T visitCreateTableStatement(CreateTableStatement e);
T visitWithClause(WithClause e);
T visitCommonTableExpression(CommonTableExpression e);
T visitOrderBy(OrderBy e);
T visitOrderingTerm(OrderingTerm e);
T visitLimit(Limit e);
@ -307,6 +322,12 @@ class RecursiveVisitor<T> extends AstVisitor<T> {
@override
T visitFrameSpec(FrameSpec e) => visitChildren(e);
@override
T visitWithClause(WithClause e) => visitChildren(e);
@override
T visitCommonTableExpression(CommonTableExpression e) => visitChildren(e);
@override
T visitMoorFile(MoorFile e) => visitChildren(e);

View File

@ -0,0 +1,79 @@
part of '../ast.dart';
class WithClause extends AstNode {
Token withToken;
final bool recursive;
Token recursiveToken;
final List<CommonTableExpression> ctes;
WithClause({@required this.recursive, @required this.ctes});
@override
T accept<T>(AstVisitor<T> visitor) => visitor.visitWithClause(this);
@override
Iterable<AstNode> get childNodes => ctes;
@override
bool contentEquals(WithClause other) => other.recursive == recursive;
}
class CommonTableExpression extends AstNode with ResultSet, VisibleToChildren {
final String cteTableName;
/// If this common table expression has explicit column names, e.g. with
/// `cnt(x) AS (...)`, contains the column names (`['x']`, in that case).
/// Otherwise null.
final List<String> columnNames;
final BaseSelectStatement as;
Token asToken;
IdentifierToken tableNameToken;
List<CommonTableExpressionColumn> _cachedColumns;
CommonTableExpression(
{@required this.cteTableName, this.columnNames, @required this.as});
@override
T accept<T>(AstVisitor<T> visitor) {
return visitor.visitCommonTableExpression(this);
}
@override
Iterable<AstNode> get childNodes => [as];
@override
bool contentEquals(CommonTableExpression other) {
return other.cteTableName == cteTableName;
}
@override
List<Column> get resolvedColumns {
final columnsOfSelect = as.resolvedColumns;
// we don't override column names, so just return the columns declared by
// the select statement
if (columnNames == null) return columnsOfSelect;
_cachedColumns ??= columnNames
.map((name) => CommonTableExpressionColumn(name, null))
.toList();
if (columnsOfSelect != null) {
// bind the CommonTableExpressionColumn to the real underlying column
// returned by the select statement
for (var i = 0; i < _cachedColumns.length; i++) {
if (i < columnsOfSelect.length) {
final selectColumn = columnsOfSelect[i];
_cachedColumns[i].innerColumn = selectColumn;
}
}
}
return _cachedColumns;
}
}

View File

@ -38,7 +38,7 @@ class TableReference extends TableOrSubquery
@override
final String as;
TableReference(this.tableName, this.as);
TableReference(this.tableName, [this.as]);
@override
Iterable<AstNode> get childNodes => const [];

View File

@ -3,7 +3,7 @@ part of '../ast.dart';
/// A subquery, which is an expression. It is expected that the inner query
/// only returns one column and one row.
class SubQuery extends Expression {
final SelectStatement select;
final BaseSelectStatement select;
SubQuery({this.select});
@ -18,7 +18,7 @@ class SubQuery extends Expression {
}
class ExistsExpression extends Expression {
final SelectStatement select;
final BaseSelectStatement select;
ExistsExpression({@required this.select});

View File

@ -1,19 +1,22 @@
part of '../ast.dart';
class DeleteStatement extends Statement
with CrudStatement
implements HasWhereClause {
class DeleteStatement extends CrudStatement implements HasWhereClause {
final TableReference from;
@override
final Expression where;
DeleteStatement({@required this.from, this.where});
DeleteStatement({WithClause withClause, @required this.from, this.where})
: super._(withClause);
@override
T accept<T>(AstVisitor<T> visitor) => visitor.visitDeleteStatement(this);
@override
Iterable<AstNode> get childNodes => [from, if (where != null) where];
Iterable<AstNode> get childNodes => [
if (withClause != null) withClause,
from,
if (where != null) where,
];
@override
bool contentEquals(DeleteStatement other) => true;

View File

@ -10,7 +10,7 @@ enum InsertMode {
insertOrIgnore
}
class InsertStatement extends Statement with CrudStatement {
class InsertStatement extends CrudStatement {
final InsertMode mode;
final TableReference table;
final List<Reference> targetColumns;
@ -28,16 +28,19 @@ class InsertStatement extends Statement with CrudStatement {
// todo parse upsert clauses
InsertStatement(
{this.mode = InsertMode.insert,
{WithClause withClause,
this.mode = InsertMode.insert,
@required this.table,
@required this.targetColumns,
@required this.source});
@required this.source})
: super._(withClause);
@override
T accept<T>(AstVisitor<T> visitor) => visitor.visitInsertStatement(this);
@override
Iterable<AstNode> get childNodes sync* {
if (withClause != null) yield withClause;
yield table;
yield* targetColumns;
yield* source.childNodes;
@ -82,7 +85,7 @@ class ValuesSource extends InsertSource {
/// Inserts the rows returned by [stmt].
class SelectInsertSource extends InsertSource {
final SelectStatement stmt;
final BaseSelectStatement stmt;
SelectInsertSource(this.stmt);

View File

@ -1,11 +1,12 @@
part of '../ast.dart';
abstract class BaseSelectStatement extends Statement
with CrudStatement, ResultSet {
abstract class BaseSelectStatement extends CrudStatement with ResultSet {
/// The resolved list of columns returned by this select statements. Not
/// available from the parse tree, will be set later by the analyzer.
@override
List<Column> resolvedColumns;
BaseSelectStatement._(WithClause withClause) : super._(withClause);
}
class SelectStatement extends BaseSelectStatement implements HasWhereClause {
@ -22,14 +23,16 @@ class SelectStatement extends BaseSelectStatement implements HasWhereClause {
final LimitBase limit;
SelectStatement(
{this.distinct = false,
{WithClause withClause,
this.distinct = false,
this.columns,
this.from,
this.where,
this.groupBy,
this.windowDeclarations = const [],
this.orderBy,
this.limit});
this.limit})
: super._(withClause);
@override
T accept<T>(AstVisitor<T> visitor) {
@ -39,6 +42,7 @@ class SelectStatement extends BaseSelectStatement implements HasWhereClause {
@override
Iterable<AstNode> get childNodes {
return [
if (withClause != null) withClause,
...columns,
if (from != null) ...from,
if (where != null) where,
@ -64,13 +68,14 @@ class CompoundSelectStatement extends BaseSelectStatement {
// part of the last compound select statement in [additional]
CompoundSelectStatement({
WithClause withClause,
@required this.base,
this.additional = const [],
});
}) : super._(withClause);
@override
Iterable<AstNode> get childNodes {
return [base, ...additional];
return [if (withClause != null) withClause, base, ...additional];
}
@override

View File

@ -4,8 +4,13 @@ abstract class Statement extends AstNode {
Token semicolon;
}
/// Marker mixin for statements that read from an existing table structure.
mixin CrudStatement on Statement {}
/// A statement that reads from an existing table structure and has an optional
/// `WITH` clause.
abstract class CrudStatement extends Statement {
WithClause withClause;
CrudStatement._(this.withClause);
}
/// Interface for statements that have a primary where clause (select, update,
/// delete).

View File

@ -16,9 +16,7 @@ const Map<TokenType, FailureMode> _tokensToMode = {
TokenType.ignore: FailureMode.ignore,
};
class UpdateStatement extends Statement
with CrudStatement
implements HasWhereClause {
class UpdateStatement extends CrudStatement implements HasWhereClause {
final FailureMode or;
final TableReference table;
final List<SetComponent> set;
@ -26,13 +24,23 @@ class UpdateStatement extends Statement
final Expression where;
UpdateStatement(
{this.or, @required this.table, @required this.set, this.where});
{WithClause withClause,
this.or,
@required this.table,
@required this.set,
this.where})
: super._(withClause);
@override
T accept<T>(AstVisitor<T> visitor) => visitor.visitUpdateStatement(this);
@override
Iterable<AstNode> get childNodes => [table, ...set, if (where != null) where];
Iterable<AstNode> get childNodes => [
if (withClause != null) withClause,
table,
...set,
if (where != null) where,
];
@override
bool contentEquals(UpdateStatement other) {

View File

@ -0,0 +1,25 @@
import 'package:sqlparser/sqlparser.dart';
/// Constructs the "sqlite_sequence" table.
Table get sqliteSequence {
final name = TableColumn('name', const ResolvedType(type: BasicType.text));
final seq = TableColumn('seq', const ResolvedType(type: BasicType.int));
return Table(name: 'sqlite_sequence', resolvedColumns: [name, seq]);
}
/// Constructs the "sqlite_master" table
Table get sqliteMaster {
final type = TableColumn('type', const ResolvedType(type: BasicType.text));
final name = TableColumn('name', const ResolvedType(type: BasicType.text));
final tblName =
TableColumn('tbl_name', const ResolvedType(type: BasicType.text));
final rootPage =
TableColumn('rootpage', const ResolvedType(type: BasicType.int));
final sql = TableColumn('sql', const ResolvedType(type: BasicType.text));
return Table(
name: 'sqlite_master',
resolvedColumns: [type, name, tblName, rootPage, sql],
);
}

View File

@ -7,6 +7,8 @@ import 'package:sqlparser/src/reader/parser/parser.dart';
import 'package:sqlparser/src/reader/tokenizer/scanner.dart';
import 'package:sqlparser/src/reader/tokenizer/token.dart';
import 'builtin_tables.dart';
class SqlEngine {
/// All tables registered with [registerTable].
final List<Table> knownTables = [];
@ -16,7 +18,10 @@ class SqlEngine {
/// extensions enabled.
final bool useMoorExtensions;
SqlEngine({this.useMoorExtensions = false});
SqlEngine({this.useMoorExtensions = false}) {
registerTable(sqliteMaster);
registerTable(sqliteSequence);
}
/// Registers the [table], which means that it can later be used in sql
/// statements.
@ -132,9 +137,8 @@ class SqlEngine {
..accept(ReferenceResolver(context))
..accept(TypeResolvingVisitor(context));
}
} catch (e) {
// todo should we do now? AFAIK, everything that causes an exception
// is added as an error contained in the context.
} catch (_) {
rethrow;
}
}
}

View File

@ -1,12 +1,95 @@
part of 'parser.dart';
mixin CrudParser on ParserBase {
CrudStatement _crud() {
final withClause = _withClause();
if (_check(TokenType.select)) {
return select(withClause: withClause);
} else if (_check(TokenType.delete)) {
return _deleteStmt(withClause);
} else if (_check(TokenType.update)) {
return _update(withClause);
} else if (_check(TokenType.insert) || _check(TokenType.replace)) {
return _insertStmt(withClause);
}
return null;
}
WithClause _withClause() {
if (!_matchOne(TokenType.$with)) return null;
final withToken = _previous;
final recursive = _matchOne(TokenType.recursive);
final recursiveToken = recursive ? _previous : null;
final ctes = <CommonTableExpression>[];
do {
final name = _consumeIdentifier('Expected name for common table');
List<String> columnNames;
// can optionally declare the column names in (foo, bar, baz) syntax
if (_matchOne(TokenType.leftParen)) {
columnNames = [];
do {
final identifier = _consumeIdentifier('Expected column name');
columnNames.add(identifier.identifier);
} while (_matchOne(TokenType.comma));
_consume(TokenType.rightParen,
'Expected closing bracket after column names');
}
final asToken = _consume(TokenType.as, 'Expected AS');
const msg = 'Expected select statement in brackets';
_consume(TokenType.leftParen, msg);
final selectStmt = select() ?? _error(msg);
_consume(TokenType.rightParen, msg);
ctes.add(CommonTableExpression(
cteTableName: name.identifier,
columnNames: columnNames,
as: selectStmt,
)
..setSpan(name, _previous)
..asToken = asToken
..tableNameToken = name);
} while (_matchOne(TokenType.comma));
return WithClause(
recursive: recursive,
ctes: ctes,
)
..setSpan(withToken, _previous)
..recursiveToken = recursiveToken
..withToken = withToken;
}
@override
BaseSelectStatement select({bool noCompound}) {
BaseSelectStatement _fullSelect() {
final clause = _withClause();
return select(withClause: clause);
}
@override
BaseSelectStatement select({bool noCompound, WithClause withClause}) {
if (noCompound == true) {
return _selectNoCompound();
return _selectNoCompound(withClause);
} else {
final first = _selectNoCompound();
final firstTokenOfBase = _peek;
final first = _selectNoCompound(withClause);
if (first == null) {
// _selectNoCompound returns null if there's no select statement at the
// current position. That's fine if we didn't encounter an with clause
// already
if (withClause != null) {
_error('Expected a SELECT statement to follow the WITH clause here');
}
return null;
}
final parts = <CompoundSelectPart>[];
while (true) {
@ -19,18 +102,24 @@ mixin CrudParser on ParserBase {
}
if (parts.isEmpty) {
// no compound parts, just return the simple select statement
// no compound parts, just return the simple select statement.
return first;
} else {
// remove with clause from base select, it belongs to the compound
// select.
first.withClause = null;
first.first = firstTokenOfBase;
return CompoundSelectStatement(
withClause: withClause,
base: first,
additional: parts,
)..setSpan(first.first, _previous);
)..setSpan(withClause?.first ?? first.first, _previous);
}
}
}
SelectStatement _selectNoCompound() {
SelectStatement _selectNoCompound([WithClause withClause]) {
if (!_match(const [TokenType.select])) return null;
final selectToken = _previous;
@ -54,7 +143,9 @@ mixin CrudParser on ParserBase {
final orderBy = _orderBy();
final limit = _limit();
final first = withClause?.first ?? selectToken;
return SelectStatement(
withClause: withClause,
distinct: distinct,
columns: resultColumns,
from: from,
@ -63,7 +154,7 @@ mixin CrudParser on ParserBase {
windowDeclarations: windowDecls,
orderBy: orderBy,
limit: limit,
)..setSpan(selectToken, _previous);
)..setSpan(first, _previous);
}
CompoundSelectPart _compoundSelectPart() {
@ -171,13 +262,15 @@ mixin CrudParser on ParserBase {
if (tableRef != null) {
return tableRef;
} else if (_matchOne(TokenType.leftParen)) {
final first = _previous;
final innerStmt = _selectNoCompound();
_consume(TokenType.rightParen,
'Expected a right bracket to terminate the inner select');
final alias = _as();
return SelectStatementAsSource(
statement: innerStmt, as: alias?.identifier);
statement: innerStmt, as: alias?.identifier)
..setSpan(first, _previous);
}
_error('Expected a table name or a nested select statement');
@ -205,6 +298,8 @@ mixin CrudParser on ParserBase {
final joins = <Join>[];
while (operator != null) {
final first = _peekNext;
final subquery = _tableOrSubquery();
final constraint = _joinConstraint();
JoinOperator resolvedOperator;
@ -227,7 +322,7 @@ mixin CrudParser on ParserBase {
operator: resolvedOperator,
query: subquery,
constraint: constraint,
));
)..setSpan(first, _previous));
// parse the next operator, if there is more than one join
if (_matchOne(TokenType.comma)) {
@ -237,7 +332,8 @@ mixin CrudParser on ParserBase {
}
}
return JoinClause(primary: start, joins: joins);
return JoinClause(primary: start, joins: joins)
..setSpan(start.first, _previous);
}
/// Parses https://www.sqlite.org/syntax/join-operator.html, minus the comma.
@ -297,6 +393,8 @@ mixin CrudParser on ParserBase {
GroupBy _groupBy() {
if (_matchOne(TokenType.group)) {
final groupToken = _previous;
_consume(TokenType.by, 'Expected a "BY"');
final by = <Expression>[];
Expression having;
@ -309,7 +407,7 @@ mixin CrudParser on ParserBase {
having = expression();
}
return GroupBy(by: by, having: having);
return GroupBy(by: by, having: having)..setSpan(groupToken, _previous);
}
return null;
}
@ -413,7 +511,7 @@ mixin CrudParser on ParserBase {
}
}
DeleteStatement _deleteStmt() {
DeleteStatement _deleteStmt([WithClause withClause]) {
if (!_matchOne(TokenType.delete)) return null;
final deleteToken = _previous;
@ -429,11 +527,14 @@ mixin CrudParser on ParserBase {
where = expression();
}
return DeleteStatement(from: table, where: where)
..setSpan(deleteToken, _previous);
return DeleteStatement(
withClause: withClause,
from: table,
where: where,
)..setSpan(withClause?.first ?? deleteToken, _previous);
}
UpdateStatement _update() {
UpdateStatement _update([WithClause withClause]) {
if (!_matchOne(TokenType.update)) return null;
final updateToken = _previous;
@ -461,11 +562,15 @@ mixin CrudParser on ParserBase {
final where = _where();
return UpdateStatement(
or: failureMode, table: table, set: set, where: where)
..setSpan(updateToken, _previous);
withClause: withClause,
or: failureMode,
table: table,
set: set,
where: where,
)..setSpan(withClause?.first ?? updateToken, _previous);
}
InsertStatement _insertStmt() {
InsertStatement _insertStmt([WithClause withClause]) {
if (!_match(const [TokenType.insert, TokenType.replace])) return null;
final firstToken = _previous;
@ -513,11 +618,12 @@ mixin CrudParser on ParserBase {
final source = _insertSource();
return InsertStatement(
withClause: withClause,
mode: insertMode,
table: table,
targetColumns: targetColumns,
source: source,
)..setSpan(firstToken, _previous);
)..setSpan(withClause?.first ?? firstToken, _previous);
}
InsertSource _insertSource() {
@ -532,7 +638,8 @@ mixin CrudParser on ParserBase {
_consume(TokenType.$values, 'Expected DEFAULT VALUES');
return const DefaultValues();
} else {
return SelectInsertSource(_selectNoCompound());
return SelectInsertSource(
_fullSelect() ?? _error('Expeced a select statement'));
}
}
@ -567,7 +674,7 @@ mixin CrudParser on ParserBase {
baseWindowName: baseWindowName,
partitionBy: partitionBy,
orderBy: orderBy,
frameSpec: spec ?? FrameSpec(),
frameSpec: spec ?? (FrameSpec()..synthetic = true),
)..setSpan(leftParen, _previous);
}

View File

@ -194,7 +194,7 @@ mixin ExpressionParser on ParserBase {
final existsToken = _previous;
_consume(
TokenType.leftParen, 'Expected opening parenthesis after EXISTS');
final selectStmt = select(noCompound: true) as SelectStatement;
final selectStmt = _fullSelect() ?? _error('Expected a select statement');
_consume(TokenType.rightParen,
'Expected closing paranthesis to finish EXISTS expression');
return ExistsExpression(select: selectStmt)
@ -263,10 +263,11 @@ mixin ExpressionParser on ParserBase {
if (_matchOne(TokenType.leftParen)) {
final left = _previous;
if (_peek.type == TokenType.select) {
final stmt = select(noCompound: true) as SelectStatement;
final selectStmt = _fullSelect(); // returns null if there's no select
if (selectStmt != null) {
_consume(TokenType.rightParen, 'Expected a closing bracket');
return SubQuery(select: stmt)..setSpan(left, _previous);
return SubQuery(select: selectStmt)..setSpan(left, _previous);
} else {
final expr = expression();
_consume(TokenType.rightParen, 'Expected a closing bracket');
@ -380,7 +381,8 @@ mixin ExpressionParser on ParserBase {
_consume(TokenType.leftParen, 'Expected opening parenthesis for tuple');
final expressions = <Expression>[];
final subQuery = select(noCompound: true) as SelectStatement;
// if desired, attempt to parse select statement
final subQuery = orSubQuery ? _fullSelect() : null;
if (subQuery == null) {
// no sub query found. read expressions that form the tuple.
// tuples can be empty `()`, so only start parsing values when it's not

View File

@ -136,7 +136,7 @@ abstract class ParserBase {
}
@alwaysThrows
void _error(String message) {
Null _error(String message) {
final error = ParsingError(_peek, message);
errors.add(error);
throw error;
@ -169,10 +169,19 @@ abstract class ParserBase {
/// [CompoundSelectStatement]. If [noCompound] is set to true, the parser will
/// only attempt to parse a [SelectStatement].
///
/// This method doesn't parse WITH clauses, most users would probably want to
/// use [_fullSelect] instead.
///
/// See also:
/// https://www.sqlite.org/lang_select.html
BaseSelectStatement select({bool noCompound});
/// Parses a select statement as defined in [the sqlite documentation][s-d],
/// which means that compound selects and a with clause is supported.
///
/// [s-d]: https://sqlite.org/syntax/select-stmt.html
BaseSelectStatement _fullSelect();
Literal _literalOrNull();
OrderingMode _orderingModeOrNull();
@ -212,17 +221,6 @@ class Parser extends ParserBase
return stmt..setSpan(first, _previous);
}
CrudStatement _crud() {
// writing select() ?? _deleteStmt() and so on doesn't cast to CrudStatement
// for some reason.
CrudStatement stmt = select();
stmt ??= _deleteStmt();
stmt ??= _update();
stmt ??= _insertStmt();
return stmt;
}
MoorFile moorFile() {
final first = _peek;
final foundComponents = <PartOfMoorFile>[];

View File

@ -121,6 +121,7 @@ enum TokenType {
create,
table,
$if,
$with,
without,
rowid,
constraint,
@ -138,6 +139,7 @@ enum TokenType {
restrict,
no,
action,
recursive,
semicolon,
comment,
@ -210,6 +212,7 @@ const Map<String, TokenType> keywords = {
'CREATE': TokenType.create,
'TABLE': TokenType.table,
'IF': TokenType.$if,
'WITH': TokenType.$with,
'WITHOUT': TokenType.without,
'ROWID': TokenType.rowid,
'CONSTRAINT': TokenType.constraint,
@ -230,6 +233,7 @@ const Map<String, TokenType> keywords = {
'OVER': TokenType.over,
'PARTITION': TokenType.partition,
'RANGE': TokenType.range,
'RECURSIVE': TokenType.recursive,
'ROWS': TokenType.rows,
'GROUPS': TokenType.groups,
'UNBOUNDED': TokenType.unbounded,

View File

@ -0,0 +1,62 @@
import 'package:sqlparser/sqlparser.dart';
import 'package:test/test.dart';
import 'data.dart';
void main() {
test('resolves columns from CTEs', () {
final engine = SqlEngine()..registerTable(demoTable);
final context = engine.analyze('''
WITH
cte (foo, bar) AS (SELECT * FROM demo)
SELECT * FROM cte;
''');
expect(context.errors, isEmpty);
final select = context.root as SelectStatement;
final types = context.types;
expect(select.resolvedColumns.map((c) => c.name), ['foo', 'bar']);
expect(
select.resolvedColumns.map((c) => types.resolveColumn(c).type),
[id.type, content.type],
);
});
test('warns on column count mismatch', () {
final engine = SqlEngine()..registerTable(demoTable);
final context = engine.analyze('''
WITH
cte (foo, bar, baz) AS (SELECT * FROM demo)
SELECT 1;
''');
expect(context.errors, hasLength(1));
final error = context.errors.single;
expect(error.type, AnalysisErrorType.cteColumnCountMismatch);
expect(error.message, stringContainsInOrder(['3', '2']));
});
test('handles recursive CTEs', () {
final engine = SqlEngine();
final context = engine.analyze('''
WITH RECURSIVE
cnt(x) AS (
SELECT 1
UNION ALL
SELECT x+1 FROM cnt
LIMIT 1000000
)
SELECT x FROM cnt;
''');
expect(context.errors, isEmpty);
final select = context.root as SelectStatement;
final column = context.typeOf(select.resolvedColumns.single);
expect(column.type, const ResolvedType(type: BasicType.int));
});
}

View File

@ -1,6 +1,14 @@
import 'package:sqlparser/sqlparser.dart';
final id = TableColumn('id', const ResolvedType(type: BasicType.int));
final id = TableColumn(
'id',
const ResolvedType(type: BasicType.int),
definition: ColumnDefinition(
columnName: 'id',
typeName: 'INTEGER',
constraints: [PrimaryKeyColumn(null)],
),
);
final content =
TableColumn('content', const ResolvedType(type: BasicType.text));

View File

@ -10,7 +10,8 @@ void main() {
final engine = SqlEngine()..registerTable(demoTable);
final context =
engine.analyze('SELECT id, d.content, *, 3 + 4 FROM demo AS d');
engine.analyze('SELECT id, d.content, *, 3 + 4 FROM demo AS d '
'WHERE _rowid_ = 3');
final select = context.root as SelectStatement;
final resolvedColumns = select.resolvedColumns;
@ -35,6 +36,9 @@ void main() {
expect((firstColumn.expression as Reference).resolved, id);
expect((secondColumn.expression as Reference).resolved, content);
expect(from.resolved, demoTable);
final where = select.where as BinaryExpression;
expect((where.left as Reference).resolved, id);
});
test('resolves the column for order by clauses', () {
@ -60,6 +64,28 @@ void main() {
);
});
group('reports correct column name for rowid aliases', () {
final engine = SqlEngine()
..registerTable(demoTable)
..registerTable(anotherTable);
test('when virtual id', () {
final context = engine.analyze('SELECT oid, _rowid_ FROM tbl');
final select = context.root as SelectStatement;
final resolvedColumns = select.resolvedColumns;
expect(resolvedColumns.map((c) => c.name), ['rowid', 'rowid']);
});
test('when alias to actual column', () {
final context = engine.analyze('SELECT oid, _rowid_ FROM demo');
final select = context.root as SelectStatement;
final resolvedColumns = select.resolvedColumns;
expect(resolvedColumns.map((c) => c.name), ['id', 'id']);
});
});
test('resolves sub-queries', () {
final engine = SqlEngine()..registerTable(demoTable);
@ -84,7 +110,7 @@ void main() {
final engine = SqlEngine()..registerTable(demoTable);
final context = engine.analyze('''
SELECT current_row() OVER wnd FROM demo
SELECT row_number() OVER wnd FROM demo
WINDOW wnd AS (PARTITION BY content GROUPS CURRENT ROW EXCLUDE TIES)
''');

View File

@ -0,0 +1,30 @@
import 'package:sqlparser/sqlparser.dart';
import 'package:test/test.dart';
void main() {
test('isAliasForRowId', () {
final engine = SqlEngine();
final schemaParser = SchemaFromCreateTable();
final isAlias = {
'CREATE TABLE x (id INTEGER PRIMARY KEY)': true,
'CREATE TABLE x (id INTEGER PRIMARY KEY) WITHOUT ROWID': false,
'CREATE TABLE x (id BIGINT PRIMARY KEY)': false,
'CREATE TABLE x (id INTEGER PRIMARY KEY DESC)': false,
'CREATE TABLE x (id INTEGER)': false,
'CREATE TABLE x (id INTEGER, PRIMARY KEY (id))': true,
};
isAlias.forEach((createTblString, isAlias) {
final parsed =
engine.parse(createTblString).rootNode as CreateTableStatement;
final table = schemaParser.read(parsed);
expect(
(table.findColumn('id') as TableColumn).isAliasForRowId(),
isAlias,
reason: '$createTblString: id is an alias? $isAlias',
);
});
});
}

View File

@ -0,0 +1,37 @@
import 'package:sqlparser/sqlparser.dart';
import 'package:test/test.dart';
void main() {
group('finds columns', () {
final engine = SqlEngine();
final schemaParser = SchemaFromCreateTable();
Column findWith(String createTbl, String columnName) {
final stmt = (engine.parse(createTbl).rootNode) as CreateTableStatement;
final table = schemaParser.read(stmt);
return table.findColumn(columnName);
}
test('when declared in table', () {
expect(findWith('CREATE TABLE x (__rowid__ VARCHAR)', '__rowid__'),
isA<TableColumn>());
});
test('when alias to rowid', () {
final column = findWith('CREATE TABLE x (id INTEGER PRIMARY KEY)', 'oid');
expect(column.name, 'id');
expect(column, isA<TableColumn>());
});
test('when virtual rowid column', () {
final column = findWith('CREATE TABLE x (id VARCHAR)', 'oid');
expect(column, isA<RowId>());
});
test('when not found', () {
final column = findWith(
'CREATE TABLE x (id INTEGER PRIMARY KEY) WITHOUT ROWID', 'oid');
expect(column, isNull);
});
});
}

Some files were not shown because too many files have changed in this diff Show More