wip: me panel.

This commit is contained in:
2022-01-07 18:04:59 +08:00
parent 048c54e080
commit 85a7d36fda
45 changed files with 2776 additions and 67 deletions
+15
View File
@@ -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';
+31
View File
@@ -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 }
+675
View File
@@ -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));
}
}
+74
View File
@@ -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();
}
}
+8
View File
@@ -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;
}
+135
View File
@@ -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);
}
+8
View File
@@ -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';
}
+425
View File
@@ -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));
}
}
+81
View File
@@ -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);
}
}
+23
View File
@@ -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);
}
+89
View File
@@ -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();
}
}
+69
View File
@@ -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();
}
}
+91
View File
@@ -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);
}
+38
View File
@@ -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)})';
}
}
+3
View File
@@ -0,0 +1,3 @@
import 'package:charcode/ascii.dart';
bool isAscii(int ch) => ch >= $nul && ch <= $del;