platform/angel_orm/lib/src/query.dart
2019-08-17 17:42:55 -04:00

322 lines
9.5 KiB
Dart

import 'dart:async';
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';
/// A SQL `SELECT` query builder.
abstract class Query<T, Where extends QueryWhere> extends QueryBase<T> {
final List<JoinBuilder> _joins = [];
final Map<String, int> _names = {};
final List<OrderBy> _orderBy = [];
String _crossJoin, _groupBy;
int _limit, _offset;
/// 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) => '$tableName.$s';
/// Returns a unique version of [name], which will not produce a collision within
/// the context of this [query].
String reserveName(String name) {
var n = _names[name] ??= 0;
_names[name]++;
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) {
int i = _joins.length;
while (true) {
var a = 'a$i';
if (trampoline.add(a)) {
return a;
} else {
i++;
}
}
}
String _compileJoin(tableName, Set<String> trampoline) {
if (tableName is String) {
return tableName;
} else if (tableName is Query) {
var c = tableName.compile(trampoline);
if (c == null) return c;
return '($c)';
} else {
throw ArgumentError.value(
tableName, 'tableName', 'must be a String or Query');
}
}
void _makeJoin(
tableName,
Set<String> trampoline,
JoinType type,
String localKey,
String foreignKey,
String op,
List<String> additionalFields) {
trampoline ??= Set();
// 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);
if (to != null) {
_joins.add(JoinBuilder(type, this, to, localKey, foreignKey,
op: op,
alias: _joinAlias(trampoline),
additionalFields: additionalFields));
}
}
/// Execute an `INNER JOIN` against another table.
void join(tableName, String localKey, String foreignKey,
{String op = '=',
List<String> additionalFields = const [],
Set<String> trampoline}) {
_makeJoin(tableName, trampoline, 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}) {
_makeJoin(tableName, trampoline, 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}) {
_makeJoin(tableName, trampoline, 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}) {
_makeJoin(tableName, trampoline, 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}) {
_makeJoin(tableName, trampoline, 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;
}
includeTableName = includeTableName || _joins.isNotEmpty;
var b = StringBuffer(preamble ?? 'SELECT');
b.write(' ');
List<String> f;
if (fields == null) {
f = ['*'];
} else {
f = List<String>.from(fields.map((s) {
var ss = includeTableName ? '$tableName.$s' : s;
var cast = casts[s];
if (cast != null) ss = 'CAST ($ss AS $cast)';
return ss;
}));
_joins.forEach((j) {
var additional = j.additionalFields.map(j.nameFor).toList();
// if (!additional.contains(j.fieldName))
// additional.insert(0, j.fieldName);
f.addAll(additional);
});
}
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 = join.compile(trampoline);
if (c != null) b.write(' $c');
}
}
var whereClause =
where.compile(tableName: includeTableName ? tableName : null);
if (whereClause.isNotEmpty) b.write(' WHERE $whereClause');
if (_limit != null) b.write(' LIMIT $_limit');
if (_offset != null) b.write(' OFFSET $_offset');
if (_groupBy != null) b.write(' GROUP BY $_groupBy');
for (var item in _orderBy) {
b.write(' ORDER BY ${item.compile()}');
}
return b.toString();
}
@override
Future<T> getOne(QueryExecutor executor) {
//limit(1);
return super.getOne(executor);
}
Future<List<T>> delete(QueryExecutor executor) {
var sql = compile(Set(), preamble: 'DELETE', withFields: false);
if (_joins.isEmpty) {
return executor
.query(tableName, sql, substitutionValues,
fields.map(adornWithTableName).toList())
.then((it) => it.map(deserialize).toList());
} else {
return executor.transaction(() async {
// TODO: Can this be done with just *one* query?
var existing = await get(executor);
//var sql = compile(preamble: 'SELECT $tableName.id', withFields: false);
return executor
.query(tableName, sql, substitutionValues)
.then((_) => existing);
});
}
}
Future<T> deleteOne(QueryExecutor executor) {
return delete(executor).then((it) => it.isEmpty ? null : it.first);
}
Future<T> insert(QueryExecutor executor) {
var insertion = values.compileInsert(this, tableName);
if (insertion == null) {
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(Set());
sql = 'WITH $tableName as ($insertion RETURNING $returning) ' + sql;
return executor
.query(tableName, sql, substitutionValues)
.then((it) => it.isEmpty ? null : deserialize(it.first));
}
}
Future<List<T>> update(QueryExecutor executor) async {
var updateSql = StringBuffer('UPDATE $tableName ');
var valuesClause = values.compileForUpdate(this);
if (valuesClause == null) {
throw StateError('No values have been specified for update.');
} else {
updateSql.write(' $valuesClause');
var whereClause = where.compile();
if (whereClause.isNotEmpty) updateSql.write(' WHERE $whereClause');
if (_limit != null) updateSql.write(' LIMIT $_limit');
var returning = fields.map(adornWithTableName).join(', ');
var sql = compile(Set());
sql = 'WITH $tableName as ($updateSql RETURNING $returning) ' + sql;
return executor
.query(tableName, sql, substitutionValues)
.then((it) => it.map(deserialize).toList());
}
}
Future<T> updateOne(QueryExecutor executor) {
return update(executor).then((it) => it.isEmpty ? null : it.first);
}
}