mirror of https://github.com/AMT-Cheif/drift.git
Merge branch 'develop' into multiple-isolates
This commit is contained in:
commit
ebc22c8382
|
@ -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 |
|
||||
|:-------------:|:-------------:|:-----:|
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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.
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
```
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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].
|
||||
|
|
|
@ -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';
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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';
|
|
@ -1,9 +1,7 @@
|
|||
import 'package:moor/moor.dart';
|
||||
import 'package:moor/src/runtime/components/component.dart';
|
||||
import 'package:moor/src/runtime/expressions/expression.dart';
|
||||
part of '../query_builder.dart';
|
||||
|
||||
/// A type for a [Join] (e.g. inner, outer).
|
||||
enum JoinType {
|
||||
enum _JoinType {
|
||||
/// Perform an inner join, see the [innerJoin] function for details.
|
||||
inner,
|
||||
|
||||
|
@ -14,27 +12,30 @@ enum JoinType {
|
|||
cross
|
||||
}
|
||||
|
||||
const Map<JoinType, String> _joinKeywords = {
|
||||
JoinType.inner: 'INNER',
|
||||
JoinType.leftOuter: 'LEFT OUTER',
|
||||
JoinType.cross: 'CROSS',
|
||||
const Map<_JoinType, String> _joinKeywords = {
|
||||
_JoinType.inner: 'INNER',
|
||||
_JoinType.leftOuter: 'LEFT OUTER',
|
||||
_JoinType.cross: 'CROSS',
|
||||
};
|
||||
|
||||
/// Used internally by moor when calling [SimpleSelectStatement.join].
|
||||
///
|
||||
/// You should use [innerJoin], [leftOuterJoin] or [crossJoin] to obtain a
|
||||
/// [Join] instance.
|
||||
class Join<T extends Table, D extends DataClass> extends Component {
|
||||
/// The [JoinType] of this join.
|
||||
final JoinType type;
|
||||
/// The [_JoinType] of this join.
|
||||
final _JoinType type;
|
||||
|
||||
/// The [TableInfo] that will be added to the query
|
||||
final TableInfo<T, D> table;
|
||||
|
||||
/// For joins that aren't [JoinType.cross], contains an additional predicate
|
||||
/// For joins that aren't [_JoinType.cross], contains an additional predicate
|
||||
/// that must be matched for the join.
|
||||
final Expression<bool, BoolType> on;
|
||||
|
||||
/// Constructs a [Join] by providing the relevant fields. [on] is optional for
|
||||
/// [JoinType.cross].
|
||||
Join(this.type, this.table, this.on);
|
||||
/// [_JoinType.cross].
|
||||
Join._(this.type, this.table, this.on);
|
||||
|
||||
@override
|
||||
void writeInto(GenerationContext context) {
|
||||
|
@ -43,7 +44,7 @@ class Join<T extends Table, D extends DataClass> extends Component {
|
|||
|
||||
context.buffer.write(table.tableWithAlias);
|
||||
|
||||
if (type != JoinType.cross) {
|
||||
if (type != _JoinType.cross) {
|
||||
context.buffer.write(' ON ');
|
||||
on.writeInto(context);
|
||||
}
|
||||
|
@ -56,7 +57,7 @@ class Join<T extends Table, D extends DataClass> extends Component {
|
|||
/// - http://www.sqlitetutorial.net/sqlite-inner-join/
|
||||
Join innerJoin<T extends Table, D extends DataClass>(
|
||||
TableInfo<T, D> other, Expression<bool, BoolType> on) {
|
||||
return Join(JoinType.inner, other, on);
|
||||
return Join._(_JoinType.inner, other, on);
|
||||
}
|
||||
|
||||
/// Creates a sql left outer join that can be used in
|
||||
|
@ -66,7 +67,7 @@ Join innerJoin<T extends Table, D extends DataClass>(
|
|||
/// - http://www.sqlitetutorial.net/sqlite-left-join/
|
||||
Join leftOuterJoin<T extends Table, D extends DataClass>(
|
||||
TableInfo<T, D> other, Expression<bool, BoolType> on) {
|
||||
return Join(JoinType.leftOuter, other, on);
|
||||
return Join._(_JoinType.leftOuter, other, on);
|
||||
}
|
||||
|
||||
/// Creates a sql cross join that can be used in
|
||||
|
@ -75,5 +76,5 @@ Join leftOuterJoin<T extends Table, D extends DataClass>(
|
|||
/// See also:
|
||||
/// - http://www.sqlitetutorial.net/sqlite-cross-join/
|
||||
Join crossJoin<T, D>(TableInfo other) {
|
||||
return Join(JoinType.cross, other, null);
|
||||
return Join._(_JoinType.cross, other, null);
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import 'package:moor/src/runtime/components/component.dart';
|
||||
part of '../query_builder.dart';
|
||||
|
||||
/// A limit clause inside a select, update or delete statement.
|
||||
class Limit extends Component {
|
|
@ -1,6 +1,4 @@
|
|||
import 'package:meta/meta.dart';
|
||||
import 'package:moor/src/runtime/components/component.dart';
|
||||
import 'package:moor/src/runtime/expressions/expression.dart';
|
||||
part of '../query_builder.dart';
|
||||
|
||||
/// Describes how to order rows
|
||||
enum OrderingMode {
|
|
@ -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 {
|
|
@ -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;
|
||||
|
|
@ -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
|
|
@ -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
|
|
@ -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.
|
|
@ -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;
|
||||
}
|
|
@ -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].
|
|
@ -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);
|
|
@ -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) {
|
|
@ -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.
|
|
@ -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 {
|
|
@ -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);
|
|
@ -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
|
||||
}
|
|
@ -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))');
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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>
|
|
@ -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',
|
||||
};
|
|
@ -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
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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.
|
|
@ -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.
|
|
@ -1,5 +1,4 @@
|
|||
import 'package:moor/moor.dart';
|
||||
import 'package:moor/src/runtime/components/component.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import 'package:moor/src/runtime/components/component.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:moor/moor.dart';
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import 'package:moor/src/runtime/components/component.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:moor/moor.dart';
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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', () {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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" }
|
|
@ -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
|
||||
|
|
|
@ -60,5 +60,6 @@ enum AnalysisErrorType {
|
|||
|
||||
unknownFunction,
|
||||
compoundColumnCountMismatch,
|
||||
cteColumnCountMismatch,
|
||||
other,
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 [];
|
||||
|
|
|
@ -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});
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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],
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>[];
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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));
|
||||
});
|
||||
}
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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)
|
||||
''');
|
||||
|
||||
|
|
|
@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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
Loading…
Reference in New Issue