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 extends QueryBase { final List _joins = []; final Map _names = {}; final List _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 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 trampoline) { int i = _joins.length; while (true) { var a = 'a$i'; if (trampoline.add(a)) { return a; } else { i++; } } } String _compileJoin(tableName, Set 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 trampoline, JoinType type, String localKey, String foreignKey, String op, List 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 additionalFields = const [], Set 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 additionalFields = const [], Set 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 additionalFields = const [], Set 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 additionalFields = const [], Set trampoline}) { _makeJoin(tableName, trampoline, JoinType.full, localKey, foreignKey, op, additionalFields); } /// Execute a `SELF JOIN`. void selfJoin(tableName, String localKey, String foreignKey, {String op = '=', List additionalFields = const [], Set trampoline}) { _makeJoin(tableName, trampoline, JoinType.self, localKey, foreignKey, op, additionalFields); } @override String compile(Set 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 f; if (fields == null) { f = ['*']; } else { f = List.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 getOne(QueryExecutor executor) { //limit(1); return super.getOne(executor); } Future> 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 deleteOne(QueryExecutor executor) { return delete(executor).then((it) => it.isEmpty ? null : it.first); } Future 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> 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 updateOne(QueryExecutor executor) { return update(executor).then((it) => it.isEmpty ? null : it.first); } }