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