parent
048c54e080
commit
85a7d36fda
@ -0,0 +1,12 @@
|
|||||||
|
Primary Authors
|
||||||
|
===============
|
||||||
|
|
||||||
|
* __[Thomas Hii](dukefirehawk.apps@gmail.com)__
|
||||||
|
|
||||||
|
Thomas is the current maintainer of the code base. He has refactored and migrated the
|
||||||
|
code base to support NNBD.
|
||||||
|
|
||||||
|
* __[Tobe O](thosakwe@gmail.com)__
|
||||||
|
|
||||||
|
Tobe has written much of the original code prior to NNBD migration. He has moved on and
|
||||||
|
is no longer involved with the project.
|
@ -0,0 +1,230 @@
|
|||||||
|
# Change Log
|
||||||
|
|
||||||
|
## 4.0.4
|
||||||
|
|
||||||
|
* Changed default varchar size to 255
|
||||||
|
* Changed default primary key to serial
|
||||||
|
|
||||||
|
## 4.0.3
|
||||||
|
|
||||||
|
* Removed debugging messages
|
||||||
|
|
||||||
|
## 4.0.2
|
||||||
|
|
||||||
|
* Updated linter to `package:lints`
|
||||||
|
* Set `createdAt` and `updatedAt` to current datetime as default
|
||||||
|
|
||||||
|
## 4.0.1
|
||||||
|
|
||||||
|
* Fixed expressions parsing error
|
||||||
|
* Fixed json data type error
|
||||||
|
* Added debug logging to sql query execution
|
||||||
|
|
||||||
|
## 4.0.0
|
||||||
|
|
||||||
|
* Updated `Optional` package
|
||||||
|
|
||||||
|
## 4.0.0-beta.4
|
||||||
|
|
||||||
|
* Added `hasSize` to `ColumnType`
|
||||||
|
|
||||||
|
## 4.0.0-beta.3
|
||||||
|
|
||||||
|
* Updated README
|
||||||
|
* Fixed NNBD issues
|
||||||
|
|
||||||
|
## 4.0.0-beta.2
|
||||||
|
|
||||||
|
* Fixed static analysis warning
|
||||||
|
|
||||||
|
## 4.0.0-beta.1
|
||||||
|
|
||||||
|
* Migrated to support Dart SDK 2.12.x NNBD
|
||||||
|
|
||||||
|
## 3.0.0
|
||||||
|
|
||||||
|
* Migrated to work with Dart SDK 2.12.x Non NNBD
|
||||||
|
|
||||||
|
## 2.1.0-beta.3
|
||||||
|
|
||||||
|
* Remove parentheses from `AS` when renaming raw `expressions`.
|
||||||
|
|
||||||
|
## 2.1.0-beta.2
|
||||||
|
|
||||||
|
* Add `expressions` to `Query`, to support custom SQL expressions that are
|
||||||
|
read as normal fields.
|
||||||
|
|
||||||
|
## 2.1.0-beta.1
|
||||||
|
|
||||||
|
* Calls to `leftJoin`, etc. alias all fields in a child query, to prevent
|
||||||
|
`ambiguous column a0.id` errors.
|
||||||
|
|
||||||
|
## 2.1.0-beta
|
||||||
|
|
||||||
|
* Split the formerly 600+ line `src/query.dart` up into
|
||||||
|
separate files.
|
||||||
|
* **BREAKING**: Add a required `QueryExecutor` argument to `transaction`
|
||||||
|
callbacks.
|
||||||
|
* Make `JoinBuilder` take `to` as a `String Function()`. This will allow
|
||||||
|
ORM queries to reference their joined subqueries.
|
||||||
|
* Removed deprecated `Join`, `toSql`, `sanitizeExpression`, `isAscii`.
|
||||||
|
* Always put `ORDER BY` before `LIMIT`.
|
||||||
|
* `and`, `or`, `not` in `QueryWhere` include parentheses.
|
||||||
|
* Add `joinType` to `Relationship` class.
|
||||||
|
|
||||||
|
## 2.0.2
|
||||||
|
|
||||||
|
* Place `LIMIT` and `OFFSET` after `ORDER BY`.
|
||||||
|
|
||||||
|
## 2.0.1
|
||||||
|
|
||||||
|
* Apply `package:pedantic` fixes.
|
||||||
|
* `@PrimaryKey()` no longer defaults to `serial`, allowing its type to be
|
||||||
|
inferenced.
|
||||||
|
|
||||||
|
## 2.0.0
|
||||||
|
|
||||||
|
* Add `isNull`, `isNotNull` getters to builders.
|
||||||
|
|
||||||
|
## 2.0.0-dev.24
|
||||||
|
|
||||||
|
* Fix a bug that caused syntax errors on `ORDER BY`.
|
||||||
|
* Add `pattern` to `like` on string builder. `sanitize` is optional.
|
||||||
|
* Add `RawSql`.
|
||||||
|
|
||||||
|
## 2.0.0-dev.23
|
||||||
|
|
||||||
|
* Add `@ManyToMany` annotation, which builds many-to-many relations.
|
||||||
|
|
||||||
|
## 2.0.0-dev.22
|
||||||
|
|
||||||
|
* `compileInsert` will explicitly never emit a key not belonging to the
|
||||||
|
associated query.
|
||||||
|
|
||||||
|
## 2.0.0-dev.21
|
||||||
|
|
||||||
|
* Add tableName to query
|
||||||
|
|
||||||
|
## 2.0.0-dev.20
|
||||||
|
|
||||||
|
* Join updates.
|
||||||
|
|
||||||
|
## 2.0.0-dev.19
|
||||||
|
|
||||||
|
* Implement cast-based `double` support.
|
||||||
|
* Finish `ListSqlExpressionBuilder`.
|
||||||
|
|
||||||
|
## 2.0.0-dev.18
|
||||||
|
|
||||||
|
* Add `ListSqlExpressionBuilder` (still in development).
|
||||||
|
|
||||||
|
## 2.0.0-dev.17
|
||||||
|
|
||||||
|
* Add `EnumSqlExpressionBuilder`.
|
||||||
|
|
||||||
|
## 2.0.0-dev.16
|
||||||
|
|
||||||
|
* Add `MapSqlExpressionBuilder` for JSON/JSONB support.
|
||||||
|
|
||||||
|
## 2.0.0-dev.15
|
||||||
|
|
||||||
|
* Remove `Column.defaultValue`.
|
||||||
|
* Deprecate `toSql` and `sanitizeExpression`.
|
||||||
|
* Refactor builders so that strings are passed through
|
||||||
|
|
||||||
|
## 2.0.0-dev.14
|
||||||
|
|
||||||
|
* Remove obsolete `@belongsToMany`.
|
||||||
|
|
||||||
|
## 2.0.0-dev.13
|
||||||
|
|
||||||
|
* Push for consistency with orm_gen @ `2.0.0-dev`.
|
||||||
|
|
||||||
|
## 2.0.0-dev.12
|
||||||
|
|
||||||
|
* Always apply `toSql` escapes.
|
||||||
|
|
||||||
|
## 2.0.0-dev.11
|
||||||
|
|
||||||
|
* Remove `limit(1)` except on `getOne`
|
||||||
|
|
||||||
|
## 2.0.0-dev.10
|
||||||
|
|
||||||
|
* Add `withFields` to `compile()`
|
||||||
|
|
||||||
|
## 2.0.0-dev.9
|
||||||
|
|
||||||
|
* Permanent preamble fix
|
||||||
|
|
||||||
|
## 2.0.0-dev.8
|
||||||
|
|
||||||
|
* Escapes
|
||||||
|
|
||||||
|
## 2.0.0-dev.7
|
||||||
|
|
||||||
|
* Update `toSql`
|
||||||
|
* Add `isTrue` and `isFalse`
|
||||||
|
|
||||||
|
## 2.0.0-dev.6
|
||||||
|
|
||||||
|
* Add `delete`, `insert` and `update` methods to `Query`.
|
||||||
|
|
||||||
|
## 2.0.0-dev.4
|
||||||
|
|
||||||
|
* Add more querying methods.
|
||||||
|
* Add preamble to `Query.compile`.
|
||||||
|
|
||||||
|
## 2.0.0-dev.3
|
||||||
|
|
||||||
|
* Brought back old-style query builder.
|
||||||
|
* Strong-mode updates, revised `Join`.
|
||||||
|
|
||||||
|
## 2.0.0-dev.2
|
||||||
|
|
||||||
|
* Renamed `ORM` to `Orm`.
|
||||||
|
* `Orm` now requires a database type.
|
||||||
|
|
||||||
|
## 2.0.0-dev.1
|
||||||
|
|
||||||
|
* Restored all old PostgreSQL-specific annotations. Rather than a smart runtime,
|
||||||
|
having a codegen capable of building ORM's for multiple databases can potentially
|
||||||
|
provide a very fast ORM for everyone.
|
||||||
|
|
||||||
|
## 2.0.0-dev
|
||||||
|
|
||||||
|
* Removed PostgreSQL-specific functionality, so that the ORM can ultimately
|
||||||
|
target all services.
|
||||||
|
* Created a better `Join` model.
|
||||||
|
* Created a far better `Query` model.
|
||||||
|
* Removed `lib/server.dart`
|
||||||
|
|
||||||
|
## 1.0.0-alpha+10
|
||||||
|
|
||||||
|
* Split into `angel_orm.dart` and `server.dart`. Prevents DDC failures.
|
||||||
|
|
||||||
|
## 1.0.0-alpha+7
|
||||||
|
|
||||||
|
* Added a `@belongsToMany` annotation class.
|
||||||
|
* Resolved [##20](https://github.com/angel-dart/orm/issues/20). The
|
||||||
|
`PostgreSQLConnectionPool` keeps track of which connections have been opened now.
|
||||||
|
|
||||||
|
## 1.0.0-alpha+6
|
||||||
|
|
||||||
|
* `DateTimeSqlExpressionBuilder` will no longer automatically
|
||||||
|
insert quotation marks around names.
|
||||||
|
|
||||||
|
## 1.0.0-alpha+5
|
||||||
|
|
||||||
|
* Corrected a typo that was causing the aforementioned test failures.
|
||||||
|
`==` becomes `=`.
|
||||||
|
|
||||||
|
## 1.0.0-alpha+4
|
||||||
|
|
||||||
|
* Added a null-check in `lib/src/query.dart##L24` to (hopefully) prevent it from
|
||||||
|
crashing on Travis.
|
||||||
|
|
||||||
|
## 1.0.0-alpha+3
|
||||||
|
|
||||||
|
* Added `isIn`, `isNotIn`, `isBetween`, `isNotBetween` to `SqlExpressionBuilder` and its
|
||||||
|
subclasses.
|
||||||
|
* Added a dependency on `package:meta`.
|
@ -0,0 +1,30 @@
|
|||||||
|
|
||||||
|
BSD 3-Clause License
|
||||||
|
|
||||||
|
Copyright (c) 2021, dukefirehawk.com
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
3. Neither the name of the copyright holder nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
@ -0,0 +1,15 @@
|
|||||||
|
# Angel3 ORM
|
||||||
|
|
||||||
|

|
||||||
|
[](https://dart.dev/null-safety)
|
||||||
|
[](https://gitter.im/angel_dart/discussion)
|
||||||
|
[](https://github.com/dukefirehawk/angel/tree/master/packages/orm/angel_orm/LICENSE)
|
||||||
|
|
||||||
|
Runtime support for Angel3 ORM. Includes a clean, database-agnostic query builder and relationship/join support.
|
||||||
|
|
||||||
|
## Supported database
|
||||||
|
|
||||||
|
* PostgreSQL version 10, 11, 12, 13 and 14
|
||||||
|
* MySQL 8.0 or later
|
||||||
|
|
||||||
|
For documentation about the ORM, see [Developer Guide](https://angel3-docs.dukefirehawk.com/guides/orm)
|
@ -0,0 +1 @@
|
|||||||
|
include: package:lints/recommended.yaml
|
@ -0,0 +1,15 @@
|
|||||||
|
export 'src/annotations.dart';
|
||||||
|
export 'src/builder.dart';
|
||||||
|
export 'src/join_builder.dart';
|
||||||
|
export 'src/join_on.dart';
|
||||||
|
export 'src/map_query_values.dart';
|
||||||
|
export 'src/migration.dart';
|
||||||
|
export 'src/order_by.dart';
|
||||||
|
export 'src/query_base.dart';
|
||||||
|
export 'src/query_executor.dart';
|
||||||
|
export 'src/query_values.dart';
|
||||||
|
export 'src/query_where.dart';
|
||||||
|
export 'src/query.dart';
|
||||||
|
export 'src/relations.dart';
|
||||||
|
export 'src/union.dart';
|
||||||
|
export 'src/util.dart';
|
@ -0,0 +1,31 @@
|
|||||||
|
/// A raw SQL statement that specifies a date/time default to the
|
||||||
|
/// current time.
|
||||||
|
const RawSql currentTimestamp = RawSql('CURRENT_TIMESTAMP');
|
||||||
|
|
||||||
|
/// Can passed to a [MigrationColumn] to default to a raw SQL expression.
|
||||||
|
class RawSql {
|
||||||
|
/// The raw SQL text.
|
||||||
|
final String value;
|
||||||
|
|
||||||
|
const RawSql(this.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Canonical instance of [ORM]. Implies all defaults.
|
||||||
|
const Orm orm = Orm();
|
||||||
|
|
||||||
|
class Orm {
|
||||||
|
/// The name of the table to query.
|
||||||
|
///
|
||||||
|
/// Inferred if not present.
|
||||||
|
final String? tableName;
|
||||||
|
|
||||||
|
/// Whether to generate migrations for this model.
|
||||||
|
///
|
||||||
|
/// Defaults to [:true:].
|
||||||
|
final bool generateMigrations;
|
||||||
|
|
||||||
|
const Orm({this.tableName, this.generateMigrations = true});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The various types of join.
|
||||||
|
enum JoinType { inner, left, right, full, self }
|
@ -0,0 +1,675 @@
|
|||||||
|
import 'package:intl/intl.dart' show DateFormat;
|
||||||
|
import 'query.dart';
|
||||||
|
|
||||||
|
final DateFormat dateYmd = DateFormat('yyyy-MM-dd');
|
||||||
|
final DateFormat dateYmdHms = DateFormat('yyyy-MM-dd HH:mm:ss');
|
||||||
|
|
||||||
|
abstract class SqlExpressionBuilder<T> {
|
||||||
|
final Query query;
|
||||||
|
final String columnName;
|
||||||
|
String? _cast;
|
||||||
|
bool _isProperty = false;
|
||||||
|
String? _substitution;
|
||||||
|
|
||||||
|
SqlExpressionBuilder(this.query, this.columnName);
|
||||||
|
|
||||||
|
String get substitution {
|
||||||
|
var c = _isProperty ? 'prop' : columnName;
|
||||||
|
return _substitution ??= query.reserveName(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get hasValue;
|
||||||
|
|
||||||
|
String? compile();
|
||||||
|
}
|
||||||
|
|
||||||
|
class NumericSqlExpressionBuilder<T extends num>
|
||||||
|
extends SqlExpressionBuilder<T> {
|
||||||
|
bool _hasValue = false;
|
||||||
|
String _op = '=';
|
||||||
|
String? _raw;
|
||||||
|
T? _value;
|
||||||
|
|
||||||
|
NumericSqlExpressionBuilder(Query query, String columnName)
|
||||||
|
: super(query, columnName);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get hasValue => _hasValue;
|
||||||
|
|
||||||
|
bool _change(String op, T value) {
|
||||||
|
_raw = null;
|
||||||
|
_op = op;
|
||||||
|
_value = value;
|
||||||
|
return _hasValue = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? compile() {
|
||||||
|
if (_raw != null) return _raw;
|
||||||
|
if (_value == null) return null;
|
||||||
|
var v = _value.toString();
|
||||||
|
if (T == double) v = 'CAST ("$v" as decimal)';
|
||||||
|
if (_cast != null) v = 'CAST ($v AS $_cast)';
|
||||||
|
return '$_op $v';
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator <(T value) => _change('<', value);
|
||||||
|
|
||||||
|
bool operator >(T value) => _change('>', value);
|
||||||
|
|
||||||
|
bool operator <=(T value) => _change('<=', value);
|
||||||
|
|
||||||
|
bool operator >=(T value) => _change('>=', value);
|
||||||
|
|
||||||
|
void get isNull {
|
||||||
|
_raw = 'IS NULL';
|
||||||
|
_hasValue = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void get isNotNull {
|
||||||
|
_raw = 'IS NOT NULL';
|
||||||
|
_hasValue = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void lessThan(T value) {
|
||||||
|
_change('<', value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void lessThanOrEqualTo(T value) {
|
||||||
|
_change('<=', value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void greaterThan(T value) {
|
||||||
|
_change('>', value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void greaterThanOrEqualTo(T value) {
|
||||||
|
_change('>=', value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void equals(T value) {
|
||||||
|
_change('=', value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void notEquals(T value) {
|
||||||
|
_change('!=', value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void isBetween(T lower, T upper) {
|
||||||
|
_raw = 'BETWEEN $lower AND $upper';
|
||||||
|
_hasValue = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void isNotBetween(T lower, T upper) {
|
||||||
|
_raw = 'NOT BETWEEN $lower AND $upper';
|
||||||
|
_hasValue = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void isIn(Iterable<T> values) {
|
||||||
|
_raw = 'IN (' + values.join(', ') + ')';
|
||||||
|
_hasValue = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void isNotIn(Iterable<T> values) {
|
||||||
|
_raw = 'NOT IN (' + values.join(', ') + ')';
|
||||||
|
_hasValue = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EnumSqlExpressionBuilder<T> extends SqlExpressionBuilder<T> {
|
||||||
|
final int Function(T) _getValue;
|
||||||
|
bool _hasValue = false;
|
||||||
|
String _op = '=';
|
||||||
|
String? _raw;
|
||||||
|
int? _value;
|
||||||
|
|
||||||
|
EnumSqlExpressionBuilder(Query query, String columnName, this._getValue)
|
||||||
|
: super(query, columnName);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get hasValue => _hasValue;
|
||||||
|
|
||||||
|
bool _change(String op, T value) {
|
||||||
|
_raw = null;
|
||||||
|
_op = op;
|
||||||
|
_value = _getValue(value);
|
||||||
|
return _hasValue = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
UnsupportedError _unsupported() =>
|
||||||
|
UnsupportedError('Enums do not support this operation.');
|
||||||
|
|
||||||
|
@override
|
||||||
|
String compile() {
|
||||||
|
if (_raw != null) {
|
||||||
|
return _raw!;
|
||||||
|
}
|
||||||
|
if (_value == null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return '$_op $_value';
|
||||||
|
}
|
||||||
|
|
||||||
|
void get isNull {
|
||||||
|
_raw = 'IS NULL';
|
||||||
|
_hasValue = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void get isNotNull {
|
||||||
|
_raw = 'IS NOT NULL';
|
||||||
|
_hasValue = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void equals(T value) {
|
||||||
|
_change('=', value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void notEquals(T value) {
|
||||||
|
_change('!=', value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void isBetween(T lower, T upper) => throw _unsupported();
|
||||||
|
|
||||||
|
void isNotBetween(T lower, T upper) => throw _unsupported();
|
||||||
|
|
||||||
|
void isIn(Iterable<T> values) {
|
||||||
|
_raw = 'IN (' + values.map(_getValue).join(', ') + ')';
|
||||||
|
_hasValue = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void isNotIn(Iterable<T> values) {
|
||||||
|
_raw = 'NOT IN (' + values.map(_getValue).join(', ') + ')';
|
||||||
|
_hasValue = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StringSqlExpressionBuilder extends SqlExpressionBuilder<String> {
|
||||||
|
bool _hasValue = false;
|
||||||
|
String? _op = '=', _raw, _value;
|
||||||
|
|
||||||
|
StringSqlExpressionBuilder(Query query, String columnName)
|
||||||
|
: super(query, columnName);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get hasValue => _hasValue;
|
||||||
|
|
||||||
|
String get lowerName => '${substitution}_lower';
|
||||||
|
|
||||||
|
String get upperName => '${substitution}_upper';
|
||||||
|
|
||||||
|
bool _change(String op, String value) {
|
||||||
|
_raw = null;
|
||||||
|
_op = op;
|
||||||
|
_value = value;
|
||||||
|
query.substitutionValues[substitution] = _value;
|
||||||
|
return _hasValue = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? compile() {
|
||||||
|
if (_raw != null) return _raw;
|
||||||
|
if (_value == null) return null;
|
||||||
|
return '$_op @$substitution';
|
||||||
|
}
|
||||||
|
|
||||||
|
void isEmpty() => equals('');
|
||||||
|
|
||||||
|
void equals(String value) {
|
||||||
|
_change('=', value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void notEquals(String value) {
|
||||||
|
_change('!=', value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a `LIKE` predicate.
|
||||||
|
///
|
||||||
|
/// To prevent injections, an optional [sanitizer] is called with a name that
|
||||||
|
/// will be escaped by the underlying [QueryExecutor]. Use this if the [pattern]
|
||||||
|
/// is not constant, and/or involves user input.
|
||||||
|
///
|
||||||
|
/// Otherwise, you can omit [sanitizer].
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// ```dart
|
||||||
|
/// carNameBuilder.like('%Mazda%');
|
||||||
|
/// carNameBuilder.like((name) => 'Mazda %$name%');
|
||||||
|
/// ```
|
||||||
|
void like(String pattern, {String Function(String)? sanitize}) {
|
||||||
|
sanitize ??= (s) => pattern;
|
||||||
|
_raw = 'LIKE \'' + sanitize('@$substitution') + '\'';
|
||||||
|
query.substitutionValues[substitution] = pattern;
|
||||||
|
_hasValue = true;
|
||||||
|
_value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void isBetween(String lower, String upper) {
|
||||||
|
query.substitutionValues[lowerName] = lower;
|
||||||
|
query.substitutionValues[upperName] = upper;
|
||||||
|
_raw = 'BETWEEN @$lowerName AND @$upperName';
|
||||||
|
_hasValue = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void isNotBetween(String lower, String upper) {
|
||||||
|
query.substitutionValues[lowerName] = lower;
|
||||||
|
query.substitutionValues[upperName] = upper;
|
||||||
|
_raw = 'NOT BETWEEN @$lowerName AND @$upperName';
|
||||||
|
_hasValue = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void get isNull {
|
||||||
|
_raw = 'IS NULL';
|
||||||
|
_hasValue = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void get isNotNull {
|
||||||
|
_raw = 'IS NOT NULL';
|
||||||
|
_hasValue = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _in(Iterable<String> values) {
|
||||||
|
return 'IN (' +
|
||||||
|
values.map((v) {
|
||||||
|
var name = query.reserveName('${columnName}_in_value');
|
||||||
|
query.substitutionValues[name] = v;
|
||||||
|
return '@$name';
|
||||||
|
}).join(', ') +
|
||||||
|
')';
|
||||||
|
}
|
||||||
|
|
||||||
|
void isIn(Iterable<String> values) {
|
||||||
|
_raw = _in(values);
|
||||||
|
_hasValue = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void isNotIn(Iterable<String> values) {
|
||||||
|
_raw = 'NOT ' + _in(values);
|
||||||
|
_hasValue = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BooleanSqlExpressionBuilder extends SqlExpressionBuilder<bool> {
|
||||||
|
bool _hasValue = false;
|
||||||
|
String? _op = '=', _raw;
|
||||||
|
bool? _value;
|
||||||
|
|
||||||
|
BooleanSqlExpressionBuilder(Query query, String columnName)
|
||||||
|
: super(query, columnName);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get hasValue => _hasValue;
|
||||||
|
|
||||||
|
bool _change(String op, bool value) {
|
||||||
|
_raw = null;
|
||||||
|
_op = op;
|
||||||
|
_value = value;
|
||||||
|
return _hasValue = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? compile() {
|
||||||
|
if (_raw != null) return _raw;
|
||||||
|
if (_value == null) return null;
|
||||||
|
var v = _value! ? 'TRUE' : 'FALSE';
|
||||||
|
if (_cast != null) v = 'CAST ($v AS $_cast)';
|
||||||
|
return '$_op $v';
|
||||||
|
}
|
||||||
|
|
||||||
|
void get isTrue => equals(true);
|
||||||
|
|
||||||
|
void get isFalse => equals(false);
|
||||||
|
|
||||||
|
void get isNull {
|
||||||
|
_raw = 'IS NULL';
|
||||||
|
_hasValue = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void get isNotNull {
|
||||||
|
_raw = 'IS NOT NULL';
|
||||||
|
_hasValue = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void equals(bool value) {
|
||||||
|
_change('=', value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void notEquals(bool value) {
|
||||||
|
_change('!=', value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DateTimeSqlExpressionBuilder extends SqlExpressionBuilder<DateTime> {
|
||||||
|
NumericSqlExpressionBuilder<int>? _year,
|
||||||
|
_month,
|
||||||
|
_day,
|
||||||
|
_hour,
|
||||||
|
_minute,
|
||||||
|
_second;
|
||||||
|
|
||||||
|
String? _raw;
|
||||||
|
|
||||||
|
DateTimeSqlExpressionBuilder(Query query, String columnName)
|
||||||
|
: super(query, columnName);
|
||||||
|
|
||||||
|
NumericSqlExpressionBuilder<int> get year =>
|
||||||
|
_year ??= NumericSqlExpressionBuilder(query, 'year');
|
||||||
|
NumericSqlExpressionBuilder<int> get month =>
|
||||||
|
_month ??= NumericSqlExpressionBuilder(query, 'month');
|
||||||
|
NumericSqlExpressionBuilder<int> get day =>
|
||||||
|
_day ??= NumericSqlExpressionBuilder(query, 'day');
|
||||||
|
NumericSqlExpressionBuilder<int> get hour =>
|
||||||
|
_hour ??= NumericSqlExpressionBuilder(query, 'hour');
|
||||||
|
NumericSqlExpressionBuilder<int> get minute =>
|
||||||
|
_minute ??= NumericSqlExpressionBuilder(query, 'minute');
|
||||||
|
NumericSqlExpressionBuilder<int> get second =>
|
||||||
|
_second ??= NumericSqlExpressionBuilder(query, 'second');
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get hasValue =>
|
||||||
|
_raw?.isNotEmpty == true ||
|
||||||
|
_year?.hasValue == true ||
|
||||||
|
_month?.hasValue == true ||
|
||||||
|
_day?.hasValue == true ||
|
||||||
|
_hour?.hasValue == true ||
|
||||||
|
_minute?.hasValue == true ||
|
||||||
|
_second?.hasValue == true;
|
||||||
|
|
||||||
|
bool _change(String _op, DateTime dt, bool time) {
|
||||||
|
var dateString = time ? dateYmdHms.format(dt) : dateYmd.format(dt);
|
||||||
|
_raw = '$columnName $_op \'$dateString\'';
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator <(DateTime value) => _change('<', value, true);
|
||||||
|
|
||||||
|
bool operator <=(DateTime value) => _change('<=', value, true);
|
||||||
|
|
||||||
|
bool operator >(DateTime value) => _change('>', value, true);
|
||||||
|
|
||||||
|
bool operator >=(DateTime value) => _change('>=', value, true);
|
||||||
|
|
||||||
|
void equals(DateTime value, {bool includeTime = true}) {
|
||||||
|
_change('=', value, includeTime != false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void lessThan(DateTime value, {bool includeTime = true}) {
|
||||||
|
_change('<', value, includeTime != false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void lessThanOrEqualTo(DateTime value, {bool includeTime = true}) {
|
||||||
|
_change('<=', value, includeTime != false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void greaterThan(DateTime value, {bool includeTime = true}) {
|
||||||
|
_change('>', value, includeTime != false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void greaterThanOrEqualTo(DateTime value, {bool includeTime = true}) {
|
||||||
|
_change('>=', value, includeTime != false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void isIn(Iterable<DateTime> values) {
|
||||||
|
_raw = '$columnName IN (' +
|
||||||
|
values.map(dateYmdHms.format).map((s) => '$s').join(', ') +
|
||||||
|
')';
|
||||||
|
}
|
||||||
|
|
||||||
|
void isNotIn(Iterable<DateTime> values) {
|
||||||
|
_raw = '$columnName NOT IN (' +
|
||||||
|
values.map(dateYmdHms.format).map((s) => '$s').join(', ') +
|
||||||
|
')';
|
||||||
|
}
|
||||||
|
|
||||||
|
void isBetween(DateTime lower, DateTime upper) {
|
||||||
|
var l = dateYmdHms.format(lower), u = dateYmdHms.format(upper);
|
||||||
|
_raw = "$columnName BETWEEN '$l' and '$u'";
|
||||||
|
}
|
||||||
|
|
||||||
|
void isNotBetween(DateTime lower, DateTime upper) {
|
||||||
|
var l = dateYmdHms.format(lower), u = dateYmdHms.format(upper);
|
||||||
|
_raw = "$columnName NOT BETWEEN '$l' and '$u'";
|
||||||
|
}
|
||||||
|
|
||||||
|
void get isNull {
|
||||||
|
_raw = '$columnName IS NULL';
|
||||||
|
}
|
||||||
|
|
||||||
|
void get isNotNull {
|
||||||
|
_raw = '$columnName IS NOT NULL';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? compile() {
|
||||||
|
if (_raw?.isNotEmpty == true) return _raw;
|
||||||
|
var parts = <String>[];
|
||||||
|
if (year.hasValue == true) {
|
||||||
|
parts.add('YEAR($columnName) ${year.compile()}');
|
||||||
|
}
|
||||||
|
if (month.hasValue == true) {
|
||||||
|
parts.add('MONTH($columnName) ${month.compile()}');
|
||||||
|
}
|
||||||
|
if (day.hasValue == true) {
|
||||||
|
parts.add('DAY($columnName) ${day.compile()}');
|
||||||
|
}
|
||||||
|
if (hour.hasValue == true) {
|
||||||
|
parts.add('HOUR($columnName) ${hour.compile()}');
|
||||||
|
}
|
||||||
|
if (minute.hasValue == true) {
|
||||||
|
parts.add('MINUTE($columnName) ${minute.compile()}');
|
||||||
|
}
|
||||||
|
if (second.hasValue == true) {
|
||||||
|
parts.add('SECOND($columnName) ${second.compile()}');
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.isEmpty ? null : parts.join(' AND ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class JsonSqlExpressionBuilder<T, K> extends SqlExpressionBuilder<T> {
|
||||||
|
final List<JsonSqlExpressionBuilderProperty> _properties = [];
|
||||||
|
bool _hasValue = false;
|
||||||
|
T? _value;
|
||||||
|
String? _op;
|
||||||
|
String? _raw;
|
||||||
|
|
||||||
|
JsonSqlExpressionBuilder(Query query, String columnName)
|
||||||
|
: super(query, columnName);
|
||||||
|
|
||||||
|
JsonSqlExpressionBuilderProperty operator [](K name) {
|
||||||
|
var p = _property(name);
|
||||||
|
_properties.add(p);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonSqlExpressionBuilderProperty _property(K name);
|
||||||
|
|
||||||
|
bool get hasRaw => _raw != null || _properties.any((p) => p.hasValue);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get hasValue => _hasValue || _properties.any((p) => p.hasValue);
|
||||||
|
|
||||||
|
T? _encodeValue(T? v) => v;
|
||||||
|
|
||||||
|
bool _change(String op, T value) {
|
||||||
|
_raw = null;
|
||||||
|
_op = op;
|
||||||
|
_value = value;
|
||||||
|
query.substitutionValues[substitution] = _encodeValue(_value);
|
||||||
|
return _hasValue = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void get isNull {
|
||||||
|
_raw = 'IS NULL';
|
||||||
|
_hasValue = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void get isNotNull {
|
||||||
|
_raw = 'IS NOT NULL';
|
||||||
|
_hasValue = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String compile() {
|
||||||
|
var s = _compile();
|
||||||
|
if (!_properties.any((p) => p.hasValue)) return s;
|
||||||
|
//s ??= '';
|
||||||
|
|
||||||
|
for (var p in _properties) {
|
||||||
|
if (p.hasValue) {
|
||||||
|
var c = p.compile();
|
||||||
|
|
||||||
|
if (c != null) {
|
||||||
|
_hasValue = true;
|
||||||
|
//s ??= '';
|
||||||
|
|
||||||
|
if (p.typed is! DateTimeSqlExpressionBuilder) {
|
||||||
|
s += '${p.typed!.columnName} ';
|
||||||
|
}
|
||||||
|
|
||||||
|
s += c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _compile() {
|
||||||
|
if (_raw != null) {
|
||||||
|
return _raw!;
|
||||||
|
}
|
||||||
|
if (_value == null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return '::jsonb $_op @$substitution::jsonb';
|
||||||
|
}
|
||||||
|
|
||||||
|
void contains(T value) {
|
||||||
|
_change('@>', value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void equals(T value) {
|
||||||
|
_change('=', value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MapSqlExpressionBuilder extends JsonSqlExpressionBuilder<Map, String> {
|
||||||
|
MapSqlExpressionBuilder(Query query, String columnName)
|
||||||
|
: super(query, columnName);
|
||||||
|
|
||||||
|
@override
|
||||||
|
JsonSqlExpressionBuilderProperty _property(String name) {
|
||||||
|
return JsonSqlExpressionBuilderProperty(this, name, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void containsKey(String key) {
|
||||||
|
this[key].isNotNull;
|
||||||
|
}
|
||||||
|
|
||||||
|
void containsPair(key, value) {
|
||||||
|
contains({key: value});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ListSqlExpressionBuilder extends JsonSqlExpressionBuilder<List, int> {
|
||||||
|
ListSqlExpressionBuilder(Query query, String columnName)
|
||||||
|
: super(query, columnName);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<dynamic>? _encodeValue(List<dynamic>? v) => v; //[json.encode(v)];
|
||||||
|
|
||||||
|
@override
|
||||||
|
JsonSqlExpressionBuilderProperty _property(int name) {
|
||||||
|
return JsonSqlExpressionBuilderProperty(this, name.toString(), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class JsonSqlExpressionBuilderProperty {
|
||||||
|
final JsonSqlExpressionBuilder builder;
|
||||||
|
final String name;
|
||||||
|
final bool isInt;
|
||||||
|
SqlExpressionBuilder? _typed;
|
||||||
|
|
||||||
|
JsonSqlExpressionBuilderProperty(this.builder, this.name, this.isInt);
|
||||||
|
|
||||||
|
SqlExpressionBuilder? get typed => _typed;
|
||||||
|
|
||||||
|
bool get hasValue => _typed?.hasValue == true;
|
||||||
|
|
||||||
|
String? compile() => _typed?.compile();
|
||||||
|
|
||||||
|
T? _set<T extends SqlExpressionBuilder?>(T Function() value) {
|
||||||
|
if (_typed is T) {
|
||||||
|
return _typed as T?;
|
||||||
|
} else if (_typed != null) {
|
||||||
|
throw StateError(
|
||||||
|
'$nameString is already typed as $_typed, and cannot be changed.');
|
||||||
|
} else {
|
||||||
|
_typed = value()
|
||||||
|
?.._cast = 'text'
|
||||||
|
.._isProperty = true;
|
||||||
|
return _typed as T?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String get nameString {
|
||||||
|
var n = isInt ? name : "'$name'";
|
||||||
|
return '${builder.columnName}::jsonb->>$n';
|
||||||
|
}
|
||||||
|
|
||||||
|
void get isNotNull {
|
||||||
|
builder
|
||||||
|
.._hasValue = true
|
||||||
|
.._raw ??= '';
|
||||||
|
|
||||||
|
var r = builder._raw;
|
||||||
|
if (r != null) {
|
||||||
|
builder._raw = r + '$nameString IS NOT NULL';
|
||||||
|
} else {
|
||||||
|
builder._raw = '$nameString IS NOT NULL';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void get isNull {
|
||||||
|
builder
|
||||||
|
.._hasValue = true
|
||||||
|
.._raw ??= '';
|
||||||
|
|
||||||
|
var r = builder._raw;
|
||||||
|
if (r != null) {
|
||||||
|
builder._raw = r + '$nameString IS NULL';
|
||||||
|
} else {
|
||||||
|
builder._raw = '$nameString IS NULL';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StringSqlExpressionBuilder? get asString {
|
||||||
|
return _set(() => StringSqlExpressionBuilder(builder.query, nameString));
|
||||||
|
}
|
||||||
|
|
||||||
|
BooleanSqlExpressionBuilder? get asBool {
|
||||||
|
return _set(() => BooleanSqlExpressionBuilder(builder.query, nameString));
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTimeSqlExpressionBuilder? get asDateTime {
|
||||||
|
return _set(() => DateTimeSqlExpressionBuilder(builder.query, nameString));
|
||||||
|
}
|
||||||
|
|
||||||
|
NumericSqlExpressionBuilder<double>? get asDouble {
|
||||||
|
return _set(
|
||||||
|
() => NumericSqlExpressionBuilder<double>(builder.query, nameString));
|
||||||
|
}
|
||||||
|
|
||||||
|
NumericSqlExpressionBuilder<int>? get asInt {
|
||||||
|
return _set(
|
||||||
|
() => NumericSqlExpressionBuilder<int>(builder.query, nameString));
|
||||||
|
}
|
||||||
|
|
||||||
|
MapSqlExpressionBuilder? get asMap {
|
||||||
|
return _set(() => MapSqlExpressionBuilder(builder.query, nameString));
|
||||||
|
}
|
||||||
|
|
||||||
|
ListSqlExpressionBuilder? get asList {
|
||||||
|
return _set(() => ListSqlExpressionBuilder(builder.query, nameString));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,74 @@
|
|||||||
|
import 'annotations.dart';
|
||||||
|
import 'query.dart';
|
||||||
|
|
||||||
|
/// Builds a SQL `JOIN` query.
|
||||||
|
class JoinBuilder {
|
||||||
|
final JoinType type;
|
||||||
|
final Query from;
|
||||||
|
final String? key, value, op, alias;
|
||||||
|
final bool aliasAllFields;
|
||||||
|
|
||||||
|
/// A callback to produces the expression to join against, i.e.
|
||||||
|
/// a table name, or the result of compiling a query.
|
||||||
|
final String Function() to;
|
||||||
|
final List<String> additionalFields;
|
||||||
|
|
||||||
|
JoinBuilder(this.type, this.from, this.to, this.key, this.value,
|
||||||
|
{this.op = '=',
|
||||||
|
this.alias,
|
||||||
|
this.additionalFields = const [],
|
||||||
|
this.aliasAllFields = false}) {
|
||||||
|
//assert(to != null,
|
||||||
|
// 'computation of this join threw an error, and returned null.');
|
||||||
|
}
|
||||||
|
|
||||||
|
String get fieldName {
|
||||||
|
var v = value;
|
||||||
|
if (aliasAllFields) {
|
||||||
|
v = '${alias}_$v';
|
||||||
|
}
|
||||||
|
var right = '${from.tableName}.$v';
|
||||||
|
if (alias != null) right = '$alias.$v';
|
||||||
|
return right;
|
||||||
|
}
|
||||||
|
|
||||||
|
String nameFor(String name) {
|
||||||
|
if (aliasAllFields) name = '${alias}_$name';
|
||||||
|
var right = '${from.tableName}.$name';
|
||||||
|
if (alias != null) right = '$alias.$name';
|
||||||
|
return right;
|
||||||
|
}
|
||||||
|
|
||||||
|
String compile(Set<String>? trampoline) {
|
||||||
|
var compiledTo = to();
|
||||||
|
//if (compiledTo == null) return null;
|
||||||
|
if (compiledTo == '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
var b = StringBuffer();
|
||||||
|
var left = '${from.tableName}.$key';
|
||||||
|
var right = fieldName;
|
||||||
|
switch (type) {
|
||||||
|
case JoinType.inner:
|
||||||
|
b.write(' INNER JOIN');
|
||||||
|
break;
|
||||||
|
case JoinType.left:
|
||||||
|
b.write(' LEFT JOIN');
|
||||||
|
break;
|
||||||
|
case JoinType.right:
|
||||||
|
b.write(' RIGHT JOIN');
|
||||||
|
break;
|
||||||
|
case JoinType.full:
|
||||||
|
b.write(' FULL OUTER JOIN');
|
||||||
|
break;
|
||||||
|
case JoinType.self:
|
||||||
|
b.write(' SELF JOIN');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
b.write(' $compiledTo');
|
||||||
|
if (alias != null) b.write(' $alias');
|
||||||
|
b.write(' ON $left$op$right');
|
||||||
|
return b.toString();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
import 'builder.dart';
|
||||||
|
|
||||||
|
class JoinOn {
|
||||||
|
final SqlExpressionBuilder key;
|
||||||
|
final SqlExpressionBuilder value;
|
||||||
|
|
||||||
|
JoinOn(this.key, this.value);
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
import 'query_values.dart';
|
||||||
|
|
||||||
|
/// A [QueryValues] implementation that simply writes to a [Map].
|
||||||
|
class MapQueryValues extends QueryValues {
|
||||||
|
final Map<String, dynamic> values = {};
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toMap() => values;
|
||||||
|
}
|
@ -0,0 +1,135 @@
|
|||||||
|
const List<String> SQL_RESERVED_WORDS = [
|
||||||
|
'SELECT',
|
||||||
|
'UPDATE',
|
||||||
|
'INSERT',
|
||||||
|
'DELETE',
|
||||||
|
'FROM',
|
||||||
|
'ASC',
|
||||||
|
'DESC',
|
||||||
|
'VALUES',
|
||||||
|
'RETURNING',
|
||||||
|
'ORDER',
|
||||||
|
'BY',
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Applies additional attributes to a database column.
|
||||||
|
class Column {
|
||||||
|
/// If `true`, a SQL field will be nullable.
|
||||||
|
final bool isNullable;
|
||||||
|
|
||||||
|
/// Specifies this column name.
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
/// Specifies the length of a `VARCHAR`.
|
||||||
|
final int length;
|
||||||
|
|
||||||
|
/// Explicitly defines a SQL type for this column.
|
||||||
|
final ColumnType type;
|
||||||
|
|
||||||
|
/// Specifies what kind of index this column is, if any.
|
||||||
|
final IndexType indexType;
|
||||||
|
|
||||||
|
/// A custom SQL expression to execute, instead of a named column.
|
||||||
|
final String? expression;
|
||||||
|
|
||||||
|
const Column(
|
||||||
|
{this.isNullable = true,
|
||||||
|
this.length = 255,
|
||||||
|
this.name = "",
|
||||||
|
this.type = ColumnType.varChar,
|
||||||
|
this.indexType = IndexType.none,
|
||||||
|
this.expression});
|
||||||
|
|
||||||
|
/// Returns `true` if [expression] is not `null`.
|
||||||
|
bool get hasExpression => expression != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
class PrimaryKey extends Column {
|
||||||
|
const PrimaryKey({ColumnType columnType = ColumnType.serial})
|
||||||
|
: super(type: columnType, indexType: IndexType.primaryKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Column primaryKey = PrimaryKey();
|
||||||
|
|
||||||
|
/// Maps to SQL index types.
|
||||||
|
enum IndexType {
|
||||||
|
none,
|
||||||
|
|
||||||
|
/// Standard index.
|
||||||
|
standardIndex,
|
||||||
|
|
||||||
|
/// A primary key.
|
||||||
|
primaryKey,
|
||||||
|
|
||||||
|
/// A *unique* index.
|
||||||
|
unique
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Maps to SQL data types.
|
||||||
|
///
|
||||||
|
/// Features all types from this list: http://www.tutorialspoint.com/sql/sql-data-types.htm
|
||||||
|
class ColumnType {
|
||||||
|
/// The name of this data type.
|
||||||
|
final String name;
|
||||||
|
final bool hasSize;
|
||||||
|
|
||||||
|
const ColumnType(this.name, [this.hasSize = false]);
|
||||||
|
|
||||||
|
static const ColumnType boolean = ColumnType('boolean');
|
||||||
|
|
||||||
|
static const ColumnType smallSerial = ColumnType('smallserial');
|
||||||
|
static const ColumnType serial = ColumnType('serial');
|
||||||
|
static const ColumnType bigSerial = ColumnType('bigserial');
|
||||||
|
|
||||||
|
// Numbers
|
||||||
|
static const ColumnType bigInt = ColumnType('bigint');
|
||||||
|
static const ColumnType int = ColumnType('int');
|
||||||
|
static const ColumnType smallInt = ColumnType('smallint');
|
||||||
|
static const ColumnType tinyInt = ColumnType('tinyint');
|
||||||
|
static const ColumnType bit = ColumnType('bit');
|
||||||
|
static const ColumnType decimal = ColumnType('decimal', true);
|
||||||
|
static const ColumnType numeric = ColumnType('numeric', true);
|
||||||
|
static const ColumnType money = ColumnType('money');
|
||||||
|
static const ColumnType smallMoney = ColumnType('smallmoney');
|
||||||
|
static const ColumnType float = ColumnType('float');
|
||||||
|
static const ColumnType real = ColumnType('real');
|
||||||
|
|
||||||
|
// Dates and times
|
||||||
|
static const ColumnType dateTime = ColumnType('datetime');
|
||||||
|
static const ColumnType smallDateTime = ColumnType('smalldatetime');
|
||||||
|
static const ColumnType date = ColumnType('date');
|
||||||
|
static const ColumnType time = ColumnType('time');
|
||||||
|
static const ColumnType timeStamp = ColumnType('timestamp');
|
||||||
|
static const ColumnType timeStampWithTimeZone =
|
||||||
|
ColumnType('timestamp with time zone');
|
||||||
|
|
||||||
|
// Strings
|
||||||
|
static const ColumnType char = ColumnType('char', true);
|
||||||
|
static const ColumnType varChar = ColumnType('varchar', true);
|
||||||
|
static const ColumnType varCharMax = ColumnType('varchar(max)');
|
||||||
|
static const ColumnType text = ColumnType('text', true);
|
||||||
|
|
||||||
|
// Unicode strings
|
||||||
|
static const ColumnType nChar = ColumnType('nchar', true);
|
||||||
|
static const ColumnType nVarChar = ColumnType('nvarchar', true);
|
||||||
|
static const ColumnType nVarCharMax = ColumnType('nvarchar(max)', true);
|
||||||
|
static const ColumnType nText = ColumnType('ntext', true);
|
||||||
|
|
||||||
|
// Binary
|
||||||
|
static const ColumnType binary = ColumnType('binary', true);
|
||||||
|
static const ColumnType varBinary = ColumnType('varbinary', true);
|
||||||
|
static const ColumnType varBinaryMax = ColumnType('varbinary(max)', true);
|
||||||
|
static const ColumnType image = ColumnType('image', true);
|
||||||
|
|
||||||
|
// JSON.
|
||||||
|
static const ColumnType json = ColumnType('json', true);
|
||||||
|
static const ColumnType jsonb = ColumnType('jsonb', true);
|
||||||
|
|
||||||
|
// Misc.
|
||||||
|
static const ColumnType sqlVariant = ColumnType('sql_variant', true);
|
||||||
|
static const ColumnType uniqueIdentifier =
|
||||||
|
ColumnType('uniqueidentifier', true);
|
||||||
|
static const ColumnType xml = ColumnType('xml', true);
|
||||||
|
static const ColumnType cursor = ColumnType('cursor', true);
|
||||||
|
static const ColumnType table = ColumnType('table', true);
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
class OrderBy {
|
||||||
|
final String key;
|
||||||
|
final bool descending;
|
||||||
|
|
||||||
|
const OrderBy(this.key, {this.descending = false});
|
||||||
|
|
||||||
|
String compile() => descending ? '$key DESC' : '$key ASC';
|
||||||
|
}
|
@ -0,0 +1,425 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
|
import 'annotations.dart';
|
||||||
|
import 'join_builder.dart';
|
||||||
|
import 'order_by.dart';
|
||||||
|
import 'query_base.dart';
|
||||||
|
import 'query_executor.dart';
|
||||||
|
import 'query_values.dart';
|
||||||
|
import 'query_where.dart';
|
||||||
|
import 'package:optional/optional.dart';
|
||||||
|
|
||||||
|
/// A SQL `SELECT` query builder.
|
||||||
|
abstract class Query<T, Where extends QueryWhere> extends QueryBase<T> {
|
||||||
|
final _log = Logger('Query');
|
||||||
|
|
||||||
|
final List<JoinBuilder> _joins = [];
|
||||||
|
final Map<String, int> _names = {};
|
||||||
|
final List<OrderBy> _orderBy = [];
|
||||||
|
|
||||||
|
// An optional "parent query". If provided, [reserveName] will operate in
|
||||||
|
// the parent's context.
|
||||||
|
final Query? parent;
|
||||||
|
|
||||||
|
/// A map of field names to explicit SQL expressions. The expressions will be aliased
|
||||||
|
/// to the given names.
|
||||||
|
final Map<String, String> expressions = {};
|
||||||
|
|
||||||
|
String? _crossJoin, _groupBy;
|
||||||
|
int? _limit, _offset;
|
||||||
|
|
||||||
|
Query({this.parent});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> get substitutionValues =>
|
||||||
|
parent?.substitutionValues ?? super.substitutionValues;
|
||||||
|
|
||||||
|
/// A reference to an abstract query builder.
|
||||||
|
///
|
||||||
|
/// This is usually a generated class.
|
||||||
|
Where? get where;
|
||||||
|
|
||||||
|
/// A set of values, for an insertion or update.
|
||||||
|
///
|
||||||
|
/// This is usually a generated class.
|
||||||
|
QueryValues? get values;
|
||||||
|
|
||||||
|
/// Preprends the [tableName] to the [String], [s].
|
||||||
|
String adornWithTableName(String s) {
|
||||||
|
if (expressions.containsKey(s)) {
|
||||||
|
//return '${expressions[s]} AS $s';
|
||||||
|
return '(${expressions[s]} AS $s)';
|
||||||
|
} else {
|
||||||
|
return '$tableName.$s';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a unique version of [name], which will not produce a collision within
|
||||||
|
/// the context of this [query].
|
||||||
|
String reserveName(String name) {
|
||||||
|
if (parent != null) {
|
||||||
|
return parent!.reserveName(name);
|
||||||
|
}
|
||||||
|
// var n = _names[name] ??= 0;
|
||||||
|
// _names[name]++;
|
||||||
|
var n = 0;
|
||||||
|
var nn = _names[name];
|
||||||
|
if (nn != null) {
|
||||||
|
n = nn;
|
||||||
|
nn++;
|
||||||
|
_names[name] = nn;
|
||||||
|
} else {
|
||||||
|
_names[name] = 1;
|
||||||
|
}
|
||||||
|
return n == 0 ? name : '$name$n';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Makes a [Where] clause.
|
||||||
|
Where newWhereClause() {
|
||||||
|
throw UnsupportedError(
|
||||||
|
'This instance does not support creating WHERE clauses.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determines whether this query can be compiled.
|
||||||
|
///
|
||||||
|
/// Used to prevent ambiguities in joins.
|
||||||
|
bool canCompile(Set<String> trampoline) => true;
|
||||||
|
|
||||||
|
/// Shorthand for calling [where].or with a [Where] clause.
|
||||||
|
void andWhere(void Function(Where) f) {
|
||||||
|
var w = newWhereClause();
|
||||||
|
f(w);
|
||||||
|
where?.and(w);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shorthand for calling [where].or with a [Where] clause.
|
||||||
|
void notWhere(void Function(Where) f) {
|
||||||
|
var w = newWhereClause();
|
||||||
|
f(w);
|
||||||
|
where?.not(w);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shorthand for calling [where].or with a [Where] clause.
|
||||||
|
void orWhere(void Function(Where) f) {
|
||||||
|
var w = newWhereClause();
|
||||||
|
f(w);
|
||||||
|
where?.or(w);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Limit the number of rows to return.
|
||||||
|
void limit(int n) {
|
||||||
|
_limit = n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Skip a number of rows in the query.
|
||||||
|
void offset(int n) {
|
||||||
|
_offset = n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Groups the results by a given key.
|
||||||
|
void groupBy(String key) {
|
||||||
|
_groupBy = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sorts the results by a key.
|
||||||
|
void orderBy(String key, {bool descending = false}) {
|
||||||
|
_orderBy.add(OrderBy(key, descending: descending));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute a `CROSS JOIN` (Cartesian product) against another table.
|
||||||
|
void crossJoin(String tableName) {
|
||||||
|
_crossJoin = tableName;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _joinAlias(Set<String> trampoline) {
|
||||||
|
var i = _joins.length;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
var a = 'a$i';
|
||||||
|
if (trampoline.add(a)) {
|
||||||
|
return a;
|
||||||
|
} else {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String Function() _compileJoin(tableName, Set<String> trampoline) {
|
||||||
|
if (tableName is String) {
|
||||||
|
return () => tableName;
|
||||||
|
} else if (tableName is Query) {
|
||||||
|
return () {
|
||||||
|
var c = tableName.compile(trampoline);
|
||||||
|
//if (c == null) return c;
|
||||||
|
if (c == '') {
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
return '($c)';
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
_log.severe('$tableName must be a String or Query');
|
||||||
|
throw ArgumentError.value(
|
||||||
|
tableName, 'tableName', 'must be a String or Query');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _makeJoin(
|
||||||
|
tableName,
|
||||||
|
Set<String>? trampoline,
|
||||||
|
String? alias,
|
||||||
|
JoinType type,
|
||||||
|
String localKey,
|
||||||
|
String foreignKey,
|
||||||
|
String op,
|
||||||
|
List<String> additionalFields) {
|
||||||
|
trampoline ??= <String>{};
|
||||||
|
|
||||||
|
// Pivot tables guard against ambiguous fields by excluding tables
|
||||||
|
// that have already been queried in this scope.
|
||||||
|
if (trampoline.contains(tableName) && trampoline.contains(this.tableName)) {
|
||||||
|
// ex. if we have {roles, role_users}, then don't join "roles" again.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var to = _compileJoin(tableName, trampoline);
|
||||||
|
alias ??= _joinAlias(trampoline);
|
||||||
|
if (tableName is Query) {
|
||||||
|
for (var field in tableName.fields) {
|
||||||
|
tableName.aliases[field] = '${alias}_$field';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_joins.add(JoinBuilder(type, this, to, localKey, foreignKey,
|
||||||
|
op: op,
|
||||||
|
alias: alias,
|
||||||
|
additionalFields: additionalFields,
|
||||||
|
aliasAllFields: tableName is Query));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute an `INNER JOIN` against another table.
|
||||||
|
void join(tableName, String localKey, String foreignKey,
|
||||||
|
{String op = '=',
|
||||||
|
List<String> additionalFields = const [],
|
||||||
|
Set<String>? trampoline,
|
||||||
|
String? alias}) {
|
||||||
|
_makeJoin(tableName, trampoline, alias, JoinType.inner, localKey, foreignKey, op,
|
||||||
|
additionalFields);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute a `LEFT JOIN` against another table.
|
||||||
|
void leftJoin(tableName, String localKey, String foreignKey,
|
||||||
|
{String op = '=',
|
||||||
|
List<String> additionalFields = const [],
|
||||||
|
Set<String>? trampoline,
|
||||||
|
String? alias}) {
|
||||||
|
_makeJoin(tableName, trampoline, alias, JoinType.left, localKey, foreignKey, op,
|
||||||
|
additionalFields);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute a `RIGHT JOIN` against another table.
|
||||||
|
void rightJoin(tableName, String localKey, String foreignKey,
|
||||||
|
{String op = '=',
|
||||||
|
List<String> additionalFields = const [],
|
||||||
|
Set<String>? trampoline,
|
||||||
|
String? alias}) {
|
||||||
|
_makeJoin(tableName, trampoline, alias, JoinType.right, localKey, foreignKey, op,
|
||||||
|
additionalFields);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute a `FULL OUTER JOIN` against another table.
|
||||||
|
void fullOuterJoin(tableName, String localKey, String foreignKey,
|
||||||
|
{String op = '=',
|
||||||
|
List<String> additionalFields = const [],
|
||||||
|
Set<String>? trampoline,
|
||||||
|
String? alias}) {
|
||||||
|
_makeJoin(tableName, trampoline, alias, JoinType.full, localKey, foreignKey, op,
|
||||||
|
additionalFields);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute a `SELF JOIN`.
|
||||||
|
void selfJoin(tableName, String localKey, String foreignKey,
|
||||||
|
{String op = '=',
|
||||||
|
List<String> additionalFields = const [],
|
||||||
|
Set<String>? trampoline,
|
||||||
|
String? alias}) {
|
||||||
|
_makeJoin(tableName, trampoline, alias, JoinType.self, localKey, foreignKey, op,
|
||||||
|
additionalFields);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String compile(Set<String> trampoline,
|
||||||
|
{bool includeTableName = false,
|
||||||
|
String? preamble,
|
||||||
|
bool withFields = true,
|
||||||
|
String? fromQuery}) {
|
||||||
|
// One table MAY appear multiple times in a query.
|
||||||
|
if (!canCompile(trampoline)) {
|
||||||
|
//return null;
|
||||||
|
//throw Exception('One table appear multiple times in a query');
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
includeTableName = includeTableName || _joins.isNotEmpty;
|
||||||
|
var b = StringBuffer(preamble ?? 'SELECT');
|
||||||
|
b.write(' ');
|
||||||
|
List<String> f;
|
||||||
|
|
||||||
|
var compiledJoins = <JoinBuilder, String?>{};
|
||||||
|
|
||||||
|
//if (fields == null) {
|
||||||
|
if (fields.isEmpty) {
|
||||||
|
f = ['*'];
|
||||||
|
} else {
|
||||||
|
f = List<String>.from(fields.map((s) {
|
||||||
|
String? ss = includeTableName ? '$tableName.$s' : s;
|
||||||
|
if (expressions.containsKey(s)) {
|
||||||
|
ss = '( ${expressions[s]} )';
|
||||||
|
//ss = expressions[s];
|
||||||
|
}
|
||||||
|
var cast = casts[s];
|
||||||
|
if (cast != null) ss = 'CAST ($ss AS $cast)';
|
||||||
|
if (aliases.containsKey(s)) {
|
||||||
|
if (cast != null) {
|
||||||
|
ss = '($ss) AS ${aliases[s]}';
|
||||||
|
} else {
|
||||||
|
ss = '$ss AS ${aliases[s]}';
|
||||||
|
}
|
||||||
|
if (expressions.containsKey(s)) {
|
||||||
|
// ss = '($ss)';
|
||||||
|
}
|
||||||
|
} else if (expressions.containsKey(s)) {
|
||||||
|
if (cast != null) {
|
||||||
|
ss = '($ss) AS $s';
|
||||||
|
// ss = '(($ss) AS $s)';
|
||||||
|
} else {
|
||||||
|
ss = '$ss AS $s';
|
||||||
|
// ss = '($ss AS $s)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ss;
|
||||||
|
}));
|
||||||
|
_joins.forEach((j) {
|
||||||
|
var c = compiledJoins[j] = j.compile(trampoline);
|
||||||
|
//if (c != null) {
|
||||||
|
if (c != '') {
|
||||||
|
var additional = j.additionalFields.map(j.nameFor).toList();
|
||||||
|
f.addAll(additional);
|
||||||
|
} else {
|
||||||
|
// If compilation failed, fill in NULL placeholders.
|
||||||
|
for (var i = 0; i < j.additionalFields.length; i++) {
|
||||||
|
f.add('NULL');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (withFields) b.write(f.join(', '));
|
||||||
|
fromQuery ??= tableName;
|
||||||
|
b.write(' FROM $fromQuery');
|
||||||
|
|
||||||
|
// No joins if it's not a select.
|
||||||
|
if (preamble == null) {
|
||||||
|
if (_crossJoin != null) b.write(' CROSS JOIN $_crossJoin');
|
||||||
|
for (var join in _joins) {
|
||||||
|
var c = compiledJoins[join];
|
||||||
|
if (c != null) b.write(' $c');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var whereClause =
|
||||||
|
where?.compile(tableName: includeTableName ? tableName : null);
|
||||||
|
if (whereClause?.isNotEmpty == true) {
|
||||||
|
b.write(' WHERE $whereClause');
|
||||||
|
}
|
||||||
|
if (_groupBy != null) b.write(' GROUP BY $_groupBy');
|
||||||
|
for (var item in _orderBy) {
|
||||||
|
b.write(' ORDER BY ${item.compile()}');
|
||||||
|
}
|
||||||
|
if (_limit != null) b.write(' LIMIT $_limit');
|
||||||
|
if (_offset != null) b.write(' OFFSET $_offset');
|
||||||
|
return b.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Optional<T>> getOne(QueryExecutor executor) {
|
||||||
|
//limit(1);
|
||||||
|
return super.getOne(executor);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<T>> delete(QueryExecutor executor) {
|
||||||
|
var sql = compile({}, preamble: 'DELETE', withFields: false);
|
||||||
|
|
||||||
|
//_log.fine("Delete Query = $sql");
|
||||||
|
|
||||||
|
if (_joins.isEmpty) {
|
||||||
|
return executor
|
||||||
|
.query(tableName, sql, substitutionValues,
|
||||||
|
fields.map(adornWithTableName).toList())
|
||||||
|
.then((it) => deserializeList(it));
|
||||||
|
} else {
|
||||||
|
return executor.transaction((tx) async {
|
||||||
|
// TODO: Can this be done with just *one* query?
|
||||||
|
var existing = await get(tx);
|
||||||
|
//var sql = compile(preamble: 'SELECT $tableName.id', withFields: false);
|
||||||
|
return tx
|
||||||
|
.query(tableName, sql, substitutionValues)
|
||||||
|
.then((_) => existing);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Optional<T>> deleteOne(QueryExecutor executor) {
|
||||||
|
return delete(executor).then((it) =>
|
||||||
|
it.isEmpty == true ? Optional.empty() : Optional.ofNullable(it.first));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Optional<T>> insert(QueryExecutor executor) {
|
||||||
|
var insertion = values?.compileInsert(this, tableName);
|
||||||
|
|
||||||
|
if (insertion == '') {
|
||||||
|
throw StateError('No values have been specified for update.');
|
||||||
|
} else {
|
||||||
|
// TODO: How to do this in a non-Postgres DB?
|
||||||
|
var returning = fields.map(adornWithTableName).join(', ');
|
||||||
|
var sql = compile({});
|
||||||
|
sql = 'WITH $tableName as ($insertion RETURNING $returning) ' + sql;
|
||||||
|
|
||||||
|
//_log.fine("Insert Query = $sql");
|
||||||
|
|
||||||
|
return executor.query(tableName, sql, substitutionValues).then((it) {
|
||||||
|
// Return SQL execution results
|
||||||
|
return it.isEmpty ? Optional.empty() : deserialize(it.first);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<T>> update(QueryExecutor executor) async {
|
||||||
|
var updateSql = StringBuffer('UPDATE $tableName ');
|
||||||
|
var valuesClause = values?.compileForUpdate(this);
|
||||||
|
|
||||||
|
if (valuesClause == '') {
|
||||||
|
throw StateError('No values have been specified for update.');
|
||||||
|
} else {
|
||||||
|
updateSql.write(' $valuesClause');
|
||||||
|
var whereClause = where?.compile();
|
||||||
|
if (whereClause?.isNotEmpty == true) {
|
||||||
|
updateSql.write(' WHERE $whereClause');
|
||||||
|
}
|
||||||
|
if (_limit != null) updateSql.write(' LIMIT $_limit');
|
||||||
|
|
||||||
|
var returning = fields.map(adornWithTableName).join(', ');
|
||||||
|
var sql = compile({});
|
||||||
|
sql = 'WITH $tableName as ($updateSql RETURNING $returning) ' + sql;
|
||||||
|
|
||||||
|
//_log.fine("Update Query = $sql");
|
||||||
|
|
||||||
|
return executor
|
||||||
|
.query(tableName, sql, substitutionValues)
|
||||||
|
.then((it) => deserializeList(it));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Optional<T>> updateOne(QueryExecutor executor) {
|
||||||
|
return update(executor).then(
|
||||||
|
(it) => it.isEmpty ? Optional.empty() : Optional.ofNullable(it.first));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,81 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'query_executor.dart';
|
||||||
|
import 'union.dart';
|
||||||
|
import 'package:optional/optional.dart';
|
||||||
|
|
||||||
|
/// A base class for objects that compile to SQL queries, typically within an ORM.
|
||||||
|
abstract class QueryBase<T> {
|
||||||
|
/// Casts to perform when querying the database.
|
||||||
|
Map<String, String> get casts => {};
|
||||||
|
|
||||||
|
/// `AS` aliases to inject into the query, if any.
|
||||||
|
Map<String, String> aliases = {};
|
||||||
|
|
||||||
|
/// Values to insert into a prepared statement.
|
||||||
|
final Map<String, dynamic> substitutionValues = {};
|
||||||
|
|
||||||
|
/// The table against which to execute this query.
|
||||||
|
String get tableName;
|
||||||
|
|
||||||
|
/// The list of fields returned by this query.
|
||||||
|
///
|
||||||
|
/// @deprecated If it's `null`, then this query will perform a `SELECT *`.
|
||||||
|
/// If it's empty, then this query will perform a `SELECT *`.
|
||||||
|
List<String> get fields;
|
||||||
|
|
||||||
|
/// A String of all [fields], joined by a comma (`,`).
|
||||||
|
String get fieldSet => fields.map((k) {
|
||||||
|
var cast = casts[k];
|
||||||
|
if (!aliases.containsKey(k)) {
|
||||||
|
return cast == null ? k : 'CAST ($k AS $cast)';
|
||||||
|
} else {
|
||||||
|
var inner = cast == null ? k : '(CAST ($k AS $cast))';
|
||||||
|
return '$inner AS ${aliases[k]}';
|
||||||
|
}
|
||||||
|
}).join(', ');
|
||||||
|
|
||||||
|
String compile(Set<String> trampoline,
|
||||||
|
{bool includeTableName = false,
|
||||||
|
String preamble = '',
|
||||||
|
bool withFields = true});
|
||||||
|
|
||||||
|
Optional<T> deserialize(List row);
|
||||||
|
|
||||||
|
List<T> deserializeList(List<List<dynamic>> it) {
|
||||||
|
var optResult = it.map(deserialize).toList();
|
||||||
|
var result = <T>[];
|
||||||
|
optResult.forEach((element) {
|
||||||
|
element.ifPresent((item) {
|
||||||
|
result.add(item);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<T>> get(QueryExecutor executor) async {
|
||||||
|
var sql = compile({});
|
||||||
|
|
||||||
|
//_log.fine('sql = $sql');
|
||||||
|
//_log.fine('substitutionValues = $substitutionValues');
|
||||||
|
|
||||||
|
return executor.query(tableName, sql, substitutionValues).then((it) {
|
||||||
|
return deserializeList(it);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Optional<T>> getOne(QueryExecutor executor) {
|
||||||
|
//return get(executor).then((it) => it.isEmpty ? : it.first);
|
||||||
|
return get(executor).then(
|
||||||
|
(it) => it.isEmpty ? Optional.empty() : Optional.ofNullable(it.first));
|
||||||
|
}
|
||||||
|
|
||||||
|
Union<T> union(QueryBase<T> other) {
|
||||||
|
return Union(this, other);
|
||||||
|
}
|
||||||
|
|
||||||
|
Union<T> unionAll(QueryBase<T> other) {
|
||||||
|
return Union(this, other, all: true);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
/// An abstract interface that performs queries.
|
||||||
|
///
|
||||||
|
/// This class should be implemented.
|
||||||
|
abstract class QueryExecutor {
|
||||||
|
const QueryExecutor();
|
||||||
|
|
||||||
|
/// Executes a single query.
|
||||||
|
Future<List<List>> query(
|
||||||
|
String tableName, String query, Map<String, dynamic> substitutionValues,
|
||||||
|
[List<String> returningFields = const []]);
|
||||||
|
|
||||||
|
/// Enters a database transaction, performing the actions within,
|
||||||
|
/// and returning the results of [f].
|
||||||
|
///
|
||||||
|
/// If [f] fails, the transaction will be rolled back, and the
|
||||||
|
/// responsible exception will be re-thrown.
|
||||||
|
///
|
||||||
|
/// Whether nested transactions are supported depends on the
|
||||||
|
/// underlying driver.
|
||||||
|
Future<T> transaction<T>(FutureOr<T> Function(QueryExecutor) f);
|
||||||
|
}
|
@ -0,0 +1,89 @@
|
|||||||
|
import 'query.dart';
|
||||||
|
|
||||||
|
abstract class QueryValues {
|
||||||
|
Map<String, String> get casts => {};
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap();
|
||||||
|
|
||||||
|
String applyCast(String name, String sub) {
|
||||||
|
if (casts.containsKey(name)) {
|
||||||
|
var type = casts[name];
|
||||||
|
return 'CAST ($sub as $type)';
|
||||||
|
} else {
|
||||||
|
return sub;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String compileInsert(Query query, String tableName) {
|
||||||
|
var data = Map<String, dynamic>.from(toMap());
|
||||||
|
var now = DateTime.now();
|
||||||
|
if (data.containsKey('created_at') && data['created_at'] == null) {
|
||||||
|
data['created_at'] = now;
|
||||||
|
}
|
||||||
|
if (data.containsKey('createdAt') && data['createdAt'] == null) {
|
||||||
|
data['createdAt'] = now;
|
||||||
|
}
|
||||||
|
if (data.containsKey('updated_at') && data['updated_at'] == null) {
|
||||||
|
data['updated_at'] = now;
|
||||||
|
}
|
||||||
|
if (data.containsKey('updatedAt') && data['updatedAt'] == null) {
|
||||||
|
data['updatedAt'] = now;
|
||||||
|
}
|
||||||
|
var keys = data.keys.toList();
|
||||||
|
keys.where((k) => !query.fields.contains(k)).forEach(data.remove);
|
||||||
|
if (data.isEmpty) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
var fieldSet = data.keys.join(', ');
|
||||||
|
var b = StringBuffer('INSERT INTO $tableName ($fieldSet) VALUES (');
|
||||||
|
var i = 0;
|
||||||
|
|
||||||
|
for (var entry in data.entries) {
|
||||||
|
if (i++ > 0) b.write(', ');
|
||||||
|
|
||||||
|
var name = query.reserveName(entry.key);
|
||||||
|
|
||||||
|
var s = applyCast(entry.key, '@$name');
|
||||||
|
query.substitutionValues[name] = entry.value;
|
||||||
|
b.write(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
b.write(')');
|
||||||
|
return b.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
String compileForUpdate(Query query) {
|
||||||
|
var data = toMap();
|
||||||
|
if (data.isEmpty) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
var now = DateTime.now();
|
||||||
|
if (data.containsKey('created_at') && data['created_at'] == null) {
|
||||||
|
data.remove('created_at');
|
||||||
|
}
|
||||||
|
if (data.containsKey('createdAt') && data['createdAt'] == null) {
|
||||||
|
data.remove('createdAt');
|
||||||
|
}
|
||||||
|
if (data.containsKey('updated_at') && data['updated_at'] == null) {
|
||||||
|
data['updated_at'] = now;
|
||||||
|
}
|
||||||
|
if (data.containsKey('updatedAt') && data['updatedAt'] == null) {
|
||||||
|
data['updatedAt'] = now;
|
||||||
|
}
|
||||||
|
var b = StringBuffer('SET');
|
||||||
|
var i = 0;
|
||||||
|
|
||||||
|
for (var entry in data.entries) {
|
||||||
|
if (i++ > 0) b.write(',');
|
||||||
|
b.write(' ');
|
||||||
|
b.write(entry.key);
|
||||||
|
b.write('=');
|
||||||
|
|
||||||
|
var name = query.reserveName(entry.key);
|
||||||
|
var s = applyCast(entry.key, '@$name');
|
||||||
|
query.substitutionValues[name] = entry.value;
|
||||||
|
b.write(s);
|
||||||
|
}
|
||||||
|
return b.toString();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
import 'builder.dart';
|
||||||
|
|
||||||
|
/// Builds a SQL `WHERE` clause.
|
||||||
|
abstract class QueryWhere {
|
||||||
|
final Set<QueryWhere> _and = {};
|
||||||
|
final Set<QueryWhere> _not = {};
|
||||||
|
final Set<QueryWhere> _or = {};
|
||||||
|
final Set<String> _raw = {};
|
||||||
|
|
||||||
|
Iterable<SqlExpressionBuilder> get expressionBuilders;
|
||||||
|
|
||||||
|
void and(QueryWhere other) {
|
||||||
|
_and.add(other);
|
||||||
|
}
|
||||||
|
|
||||||
|
void not(QueryWhere other) {
|
||||||
|
_not.add(other);
|
||||||
|
}
|
||||||
|
|
||||||
|
void or(QueryWhere other) {
|
||||||
|
_or.add(other);
|
||||||
|
}
|
||||||
|
|
||||||
|
void raw(String whereRaw) {
|
||||||
|
_raw.add(whereRaw);
|
||||||
|
}
|
||||||
|
|
||||||
|
String compile({String? tableName}) {
|
||||||
|
var b = StringBuffer();
|
||||||
|
var i = 0;
|
||||||
|
|
||||||
|
for (var builder in expressionBuilders) {
|
||||||
|
var key = builder.columnName;
|
||||||
|
if (tableName != null) key = '$tableName.$key';
|
||||||
|
if (builder.hasValue) {
|
||||||
|
if (i++ > 0) b.write(' AND ');
|
||||||
|
if (builder is DateTimeSqlExpressionBuilder ||
|
||||||
|
(builder is JsonSqlExpressionBuilder && builder.hasRaw)) {
|
||||||
|
if (tableName != null) b.write('$tableName.');
|
||||||
|
b.write(builder.compile());
|
||||||
|
} else {
|
||||||
|
b.write('$key ${builder.compile()}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var raw in _raw) {
|
||||||
|
if (i++ > 0) b.write(' AND ');
|
||||||
|
b.write(' ($raw)');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var other in _and) {
|
||||||
|
var sql = other.compile();
|
||||||
|
if (sql.isNotEmpty) b.write(' AND ($sql)');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var other in _not) {
|
||||||
|
var sql = other.compile();
|
||||||
|
if (sql.isNotEmpty) b.write(' NOT ($sql)');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var other in _or) {
|
||||||
|
var sql = other.compile();
|
||||||
|
if (sql.isNotEmpty) b.write(' OR ($sql)');
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.toString();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,91 @@
|
|||||||
|
import 'annotations.dart';
|
||||||
|
|
||||||
|
abstract class RelationshipType {
|
||||||
|
static const int hasMany = 0;
|
||||||
|
static const int hasOne = 1;
|
||||||
|
static const int belongsTo = 2;
|
||||||
|
static const int manyToMany = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Relationship {
|
||||||
|
final int type;
|
||||||
|
final String? localKey;
|
||||||
|
final String? foreignKey;
|
||||||
|
final String? foreignTable;
|
||||||
|
final bool cascadeOnDelete;
|
||||||
|
final JoinType? joinType;
|
||||||
|
|
||||||
|
const Relationship(this.type,
|
||||||
|
{this.localKey,
|
||||||
|
this.foreignKey,
|
||||||
|
this.foreignTable,
|
||||||
|
this.cascadeOnDelete = false,
|
||||||
|
this.joinType});
|
||||||
|
}
|
||||||
|
|
||||||
|
class HasMany extends Relationship {
|
||||||
|
const HasMany(
|
||||||
|
{String? localKey,
|
||||||
|
String? foreignKey,
|
||||||
|
String? foreignTable,
|
||||||
|
bool cascadeOnDelete = false,
|
||||||
|
JoinType? joinType})
|
||||||
|
: super(RelationshipType.hasMany,
|
||||||
|
localKey: localKey,
|
||||||
|
foreignKey: foreignKey,
|
||||||
|
foreignTable: foreignTable,
|
||||||
|
cascadeOnDelete: cascadeOnDelete == true,
|
||||||
|
joinType: joinType);
|
||||||
|
}
|
||||||
|
|
||||||
|
const HasMany hasMany = HasMany();
|
||||||
|
|
||||||
|
class HasOne extends Relationship {
|
||||||
|
const HasOne(
|
||||||
|
{String? localKey,
|
||||||
|
String? foreignKey,
|
||||||
|
String? foreignTable,
|
||||||
|
bool cascadeOnDelete = false,
|
||||||
|
JoinType? joinType})
|
||||||
|
: super(RelationshipType.hasOne,
|
||||||
|
localKey: localKey,
|
||||||
|
foreignKey: foreignKey,
|
||||||
|
foreignTable: foreignTable,
|
||||||
|
cascadeOnDelete: cascadeOnDelete == true,
|
||||||
|
joinType: joinType);
|
||||||
|
}
|
||||||
|
|
||||||
|
const HasOne hasOne = HasOne();
|
||||||
|
|
||||||
|
class BelongsTo extends Relationship {
|
||||||
|
const BelongsTo(
|
||||||
|
{String? localKey,
|
||||||
|
String? foreignKey,
|
||||||
|
String? foreignTable,
|
||||||
|
JoinType? joinType})
|
||||||
|
: super(RelationshipType.belongsTo,
|
||||||
|
localKey: localKey,
|
||||||
|
foreignKey: foreignKey,
|
||||||
|
foreignTable: foreignTable,
|
||||||
|
joinType: joinType);
|
||||||
|
}
|
||||||
|
|
||||||
|
const BelongsTo belongsTo = BelongsTo();
|
||||||
|
|
||||||
|
class ManyToMany extends Relationship {
|
||||||
|
final Type through;
|
||||||
|
|
||||||
|
const ManyToMany(this.through,
|
||||||
|
{String? localKey,
|
||||||
|
String? foreignKey,
|
||||||
|
String? foreignTable,
|
||||||
|
bool cascadeOnDelete = false,
|
||||||
|
JoinType? joinType})
|
||||||
|
: super(
|
||||||
|
RelationshipType.hasMany, // Many-to-Many is actually just a hasMany
|
||||||
|
localKey: localKey,
|
||||||
|
foreignKey: foreignKey,
|
||||||
|
foreignTable: foreignTable,
|
||||||
|
cascadeOnDelete: cascadeOnDelete == true,
|
||||||
|
joinType: joinType);
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
import 'query_base.dart';
|
||||||
|
import 'package:optional/optional.dart';
|
||||||
|
|
||||||
|
/// Represents the `UNION` of two subqueries.
|
||||||
|
class Union<T> extends QueryBase<T> {
|
||||||
|
/// The subject(s) of this binary operation.
|
||||||
|
final QueryBase<T> left, right;
|
||||||
|
|
||||||
|
/// Whether this is a `UNION ALL` operation.
|
||||||
|
final bool all;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String tableName;
|
||||||
|
|
||||||
|
Union(this.left, this.right, {this.all = false, String? tableName})
|
||||||
|
: tableName = tableName ?? left.tableName {
|
||||||
|
substitutionValues
|
||||||
|
..addAll(left.substitutionValues)
|
||||||
|
..addAll(right.substitutionValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<String> get fields => left.fields;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Optional<T> deserialize(List row) => left.deserialize(row);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String compile(Set<String> trampoline,
|
||||||
|
{bool includeTableName = false,
|
||||||
|
String? preamble,
|
||||||
|
bool withFields = true}) {
|
||||||
|
var selector = all == true ? 'UNION ALL' : 'UNION';
|
||||||
|
var t1 = Set<String>.from(trampoline);
|
||||||
|
var t2 = Set<String>.from(trampoline);
|
||||||
|
return '(${left.compile(t1, includeTableName: includeTableName)}) $selector (${right.compile(t2, includeTableName: includeTableName)})';
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
import 'package:charcode/ascii.dart';
|
||||||
|
|
||||||
|
bool isAscii(int ch) => ch >= $nul && ch <= $del;
|
@ -0,0 +1,21 @@
|
|||||||
|
name: angel3_orm
|
||||||
|
version: 4.0.4
|
||||||
|
description: Runtime support for Angel3 ORM. Includes base classes for queries.
|
||||||
|
homepage: https://angel3-framework.web.app/
|
||||||
|
repository: https://github.com/dukefirehawk/angel/tree/master/packages/orm/angel_orm
|
||||||
|
environment:
|
||||||
|
sdk: '>=2.12.0 <3.0.0'
|
||||||
|
dependencies:
|
||||||
|
charcode: ^1.2.0
|
||||||
|
intl: ^0.17.0
|
||||||
|
meta: ^1.3.0
|
||||||
|
string_scanner: ^1.1.0
|
||||||
|
optional: ^6.0.0
|
||||||
|
logging: ^1.0.0
|
||||||
|
dev_dependencies:
|
||||||
|
angel3_model: ^3.0.0
|
||||||
|
angel3_serialize: ^4.1.0
|
||||||
|
angel3_serialize_generator: ^4.1.0
|
||||||
|
build_runner: ^2.1.1
|
||||||
|
test: ^1.17.4
|
||||||
|
lints: ^1.0.0
|
@ -0,0 +1,19 @@
|
|||||||
|
import 'package:angel3_orm/angel3_orm.dart';
|
||||||
|
import 'package:angel3_serialize/angel3_serialize.dart';
|
||||||
|
import 'package:dde_gesture_manager_api/src/models/base_model.dart';
|
||||||
|
import 'package:angel3_migration/angel3_migration.dart';
|
||||||
|
import 'package:optional/optional.dart';
|
||||||
|
|
||||||
|
part 'download_history.g.dart';
|
||||||
|
|
||||||
|
@serializable
|
||||||
|
@orm
|
||||||
|
abstract class _DownloadHistory extends BaseModel {
|
||||||
|
@Column(isNullable: false, indexType: IndexType.standardIndex)
|
||||||
|
@SerializableField(isNullable: false)
|
||||||
|
int? get uid;
|
||||||
|
|
||||||
|
@Column(isNullable: false, indexType: IndexType.standardIndex)
|
||||||
|
@SerializableField(isNullable: false)
|
||||||
|
int? get schemeId;
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
import 'package:angel3_orm/angel3_orm.dart';
|
||||||
|
import 'package:angel3_serialize/angel3_serialize.dart';
|
||||||
|
import 'package:dde_gesture_manager_api/src/models/base_model.dart';
|
||||||
|
import 'package:angel3_migration/angel3_migration.dart';
|
||||||
|
import 'package:optional/optional.dart';
|
||||||
|
|
||||||
|
part 'like_record.g.dart';
|
||||||
|
|
||||||
|
@serializable
|
||||||
|
@orm
|
||||||
|
abstract class _LikeRecord extends BaseModel {
|
||||||
|
@Column(isNullable: false, indexType: IndexType.standardIndex)
|
||||||
|
@SerializableField(isNullable: false)
|
||||||
|
int? get uid;
|
||||||
|
|
||||||
|
@Column(isNullable: false, indexType: IndexType.standardIndex)
|
||||||
|
@SerializableField(isNullable: false)
|
||||||
|
int? get schemeId;
|
||||||
|
|
||||||
|
@Column(isNullable: false, indexType: IndexType.standardIndex)
|
||||||
|
@SerializableField(isNullable: false)
|
||||||
|
bool? get liked;
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
import 'dart:collection';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
|
class _SimpleThrottleNode {
|
||||||
|
int funcHashCode;
|
||||||
|
int timestamp;
|
||||||
|
|
||||||
|
_SimpleThrottleNode(this.funcHashCode, this.timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef void _VoidFunc();
|
||||||
|
|
||||||
|
final _simpleThrottleQueue = Queue();
|
||||||
|
|
||||||
|
/// Usage: If you have a function : test(int n) => n;
|
||||||
|
/// you can use SimpleThrottle.throttledFunc(test)?.call(1) to make it throttled
|
||||||
|
/// this will return function's return value if last call time over the timeout
|
||||||
|
/// otherwise this will return null.
|
||||||
|
/// If your function is a 'void function()', you can use SimpleThrottle.invoke(func) to call it throttled,
|
||||||
|
/// and you can get a throttled function by SimpleThrottle.bind(func) if you do not call it immediately.
|
||||||
|
class SimpleThrottle {
|
||||||
|
static T? throttledFunc<T extends Function>(T func,
|
||||||
|
{String? funcKey, Duration timeout = const Duration(seconds: 1)}) {
|
||||||
|
var node = _simpleThrottleQueue.firstWhereOrNull((element) => element.funcHashCode == (funcKey ?? func).hashCode);
|
||||||
|
if (node != null) {
|
||||||
|
if (DateTime.now().millisecondsSinceEpoch - node.timestamp < timeout.inMilliseconds)
|
||||||
|
return null;
|
||||||
|
else
|
||||||
|
node.timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
} else {
|
||||||
|
_simpleThrottleQueue.add(_SimpleThrottleNode((funcKey ?? func).hashCode, DateTime.now().millisecondsSinceEpoch));
|
||||||
|
while (_simpleThrottleQueue.length > 16) {
|
||||||
|
_simpleThrottleQueue.removeFirst();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return func;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void invoke(_VoidFunc func, {String? funcKey, Duration timeout = const Duration(seconds: 1)}) =>
|
||||||
|
throttledFunc(func, timeout: timeout, funcKey: funcKey)?.call();
|
||||||
|
|
||||||
|
static _VoidFunc bind(_VoidFunc func, {String? funcKey, Duration timeout = const Duration(seconds: 1)}) =>
|
||||||
|
() => invoke(func, timeout: timeout, funcKey: funcKey);
|
||||||
|
}
|
Loading…
Reference in new issue