Add joins
This commit is contained in:
parent
cb73f4a112
commit
34d949d0e2
4 changed files with 172 additions and 33 deletions
|
@ -11,6 +11,8 @@ main() async {
|
|||
..lastName.equals('Person')
|
||||
..or(new EmployeeQueryWhere()..salary.greaterThanOrEqualTo(75000));
|
||||
|
||||
query.join('companies', 'company_id', 'id');
|
||||
|
||||
var richPerson = await query.getOne(new _FakeExecutor());
|
||||
print(richPerson.toJson());
|
||||
}
|
||||
|
@ -63,26 +65,21 @@ class EmployeeQuery extends Query<Employee, EmployeeQueryWhere> {
|
|||
|
||||
class EmployeeQueryWhere extends QueryWhere {
|
||||
@override
|
||||
Map<String, SqlExpressionBuilder> get expressionBuilders {
|
||||
return {
|
||||
'id': id,
|
||||
'first_name': firstName,
|
||||
'last_name': lastName,
|
||||
'salary': salary,
|
||||
'created_at': createdAt,
|
||||
'updated_at': updatedAt
|
||||
};
|
||||
Iterable<SqlExpressionBuilder> get expressionBuilders {
|
||||
return [id, firstName, lastName, salary, createdAt, updatedAt];
|
||||
}
|
||||
|
||||
final NumericSqlExpressionBuilder<int> id =
|
||||
new NumericSqlExpressionBuilder<int>();
|
||||
new NumericSqlExpressionBuilder<int>('id');
|
||||
|
||||
final StringSqlExpressionBuilder firstName = new StringSqlExpressionBuilder();
|
||||
final StringSqlExpressionBuilder firstName =
|
||||
new StringSqlExpressionBuilder('first_name');
|
||||
|
||||
final StringSqlExpressionBuilder lastName = new StringSqlExpressionBuilder();
|
||||
final StringSqlExpressionBuilder lastName =
|
||||
new StringSqlExpressionBuilder('last_name');
|
||||
|
||||
final NumericSqlExpressionBuilder<double> salary =
|
||||
new NumericSqlExpressionBuilder<double>();
|
||||
new NumericSqlExpressionBuilder<double>('salary');
|
||||
|
||||
final DateTimeSqlExpressionBuilder createdAt =
|
||||
new DateTimeSqlExpressionBuilder('created_at');
|
||||
|
|
|
@ -15,4 +15,4 @@ class CanJoin {
|
|||
}
|
||||
|
||||
/// The various types of [Join].
|
||||
enum JoinType { join, left, right, full, self }
|
||||
enum JoinType { inner, left, right, full, self }
|
||||
|
|
|
@ -28,6 +28,8 @@ String sanitizeExpression(String unsafe) {
|
|||
}
|
||||
|
||||
abstract class SqlExpressionBuilder<T> {
|
||||
String get columnName;
|
||||
|
||||
bool get hasValue;
|
||||
|
||||
String compile();
|
||||
|
@ -43,11 +45,14 @@ abstract class SqlExpressionBuilder<T> {
|
|||
|
||||
class NumericSqlExpressionBuilder<T extends num>
|
||||
implements SqlExpressionBuilder<T> {
|
||||
final String columnName;
|
||||
bool _hasValue = false;
|
||||
String _op = '=';
|
||||
String _raw;
|
||||
T _value;
|
||||
|
||||
NumericSqlExpressionBuilder(this.columnName);
|
||||
|
||||
@override
|
||||
bool get hasValue => _hasValue;
|
||||
|
||||
|
@ -123,9 +128,12 @@ class NumericSqlExpressionBuilder<T extends num>
|
|||
}
|
||||
|
||||
class StringSqlExpressionBuilder implements SqlExpressionBuilder<String> {
|
||||
final String columnName;
|
||||
bool _hasValue = false;
|
||||
String _op = '=', _raw, _value;
|
||||
|
||||
StringSqlExpressionBuilder(this.columnName);
|
||||
|
||||
@override
|
||||
bool get hasValue => _hasValue;
|
||||
|
||||
|
@ -190,10 +198,13 @@ class StringSqlExpressionBuilder implements SqlExpressionBuilder<String> {
|
|||
}
|
||||
|
||||
class BooleanSqlExpressionBuilder implements SqlExpressionBuilder<bool> {
|
||||
final String columnName;
|
||||
bool _hasValue = false;
|
||||
String _op = '=', _raw;
|
||||
bool _value;
|
||||
|
||||
BooleanSqlExpressionBuilder(this.columnName);
|
||||
|
||||
@override
|
||||
bool get hasValue => _hasValue;
|
||||
|
||||
|
@ -243,12 +254,12 @@ class BooleanSqlExpressionBuilder implements SqlExpressionBuilder<bool> {
|
|||
|
||||
class DateTimeSqlExpressionBuilder implements SqlExpressionBuilder<DateTime> {
|
||||
final NumericSqlExpressionBuilder<int> year =
|
||||
new NumericSqlExpressionBuilder<int>(),
|
||||
month = new NumericSqlExpressionBuilder<int>(),
|
||||
day = new NumericSqlExpressionBuilder<int>(),
|
||||
hour = new NumericSqlExpressionBuilder<int>(),
|
||||
minute = new NumericSqlExpressionBuilder<int>(),
|
||||
second = new NumericSqlExpressionBuilder<int>();
|
||||
new NumericSqlExpressionBuilder<int>('year'),
|
||||
month = new NumericSqlExpressionBuilder<int>('month'),
|
||||
day = new NumericSqlExpressionBuilder<int>('day'),
|
||||
hour = new NumericSqlExpressionBuilder<int>('hour'),
|
||||
minute = new NumericSqlExpressionBuilder<int>('minute'),
|
||||
second = new NumericSqlExpressionBuilder<int>('second');
|
||||
final String columnName;
|
||||
String _raw;
|
||||
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import 'dart:async';
|
||||
import 'annotations.dart';
|
||||
import 'builder.dart';
|
||||
|
||||
/// A base class for objects that compile to SQL queries, typically within an ORM.
|
||||
abstract class QueryBase<T> {
|
||||
String compile();
|
||||
String compile({bool includeTableName: false});
|
||||
|
||||
T deserialize(List row);
|
||||
|
||||
|
@ -25,8 +26,22 @@ abstract class QueryBase<T> {
|
|||
}
|
||||
}
|
||||
|
||||
class OrderBy {
|
||||
final String key;
|
||||
final bool descending;
|
||||
|
||||
const OrderBy(this.key, {this.descending: false});
|
||||
|
||||
String compile() => descending ? '$key DESC' : '$key ASC';
|
||||
}
|
||||
|
||||
/// A SQL `SELECT` query builder.
|
||||
abstract class Query<T, Where extends QueryWhere> extends QueryBase<T> {
|
||||
final List<OrderBy> _orderBy = [];
|
||||
String _crossJoin, _groupBy;
|
||||
int _limit, _offset;
|
||||
Join _join;
|
||||
|
||||
/// The table against which to execute this query.
|
||||
String get tableName;
|
||||
|
||||
|
@ -40,16 +55,82 @@ abstract class Query<T, Where extends QueryWhere> extends QueryBase<T> {
|
|||
/// This is often a generated class.
|
||||
Where get where;
|
||||
|
||||
/// 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(new OrderBy(key, descending: descending));
|
||||
}
|
||||
|
||||
/// Execute a `CROSS JOIN` (Cartesian product) against another table.
|
||||
void crossJoin(String tableName) {
|
||||
_crossJoin = tableName;
|
||||
}
|
||||
|
||||
/// Execute an `INNER JOIN` against another table.
|
||||
void join(String tableName, String localKey, String foreignKey,
|
||||
{String op: '='}) {
|
||||
_join =
|
||||
new Join(JoinType.inner, this, tableName, localKey, foreignKey, op: op);
|
||||
}
|
||||
|
||||
/// Execute a `LEFT JOIN` against another table.
|
||||
void leftJoin(String tableName, String localKey, String foreignKey,
|
||||
{String op: '='}) {
|
||||
_join =
|
||||
new Join(JoinType.left, this, tableName, localKey, foreignKey, op: op);
|
||||
}
|
||||
|
||||
/// Execute a `RIGHT JOIN` against another table.
|
||||
void rightJoin(String tableName, String localKey, String foreignKey,
|
||||
{String op: '='}) {
|
||||
_join =
|
||||
new Join(JoinType.right, this, tableName, localKey, foreignKey, op: op);
|
||||
}
|
||||
|
||||
/// Execute a `FULL OUTER JOIN` against another table.
|
||||
void fullOuterJoin(String tableName, String localKey, String foreignKey,
|
||||
{String op: '='}) {
|
||||
_join =
|
||||
new Join(JoinType.full, this, tableName, localKey, foreignKey, op: op);
|
||||
}
|
||||
|
||||
/// Execute a `SELF JOIN`.
|
||||
void selfJoin(String tableName, String localKey, String foreignKey,
|
||||
{String op: '='}) {
|
||||
_join =
|
||||
new Join(JoinType.self, this, tableName, localKey, foreignKey, op: op);
|
||||
}
|
||||
|
||||
@override
|
||||
String compile() {
|
||||
String compile({bool includeTableName: false}) {
|
||||
var b = new StringBuffer('SELECT ');
|
||||
if (fields == null)
|
||||
b.write('*');
|
||||
else
|
||||
b.write(fields.join(', '));
|
||||
var f = fields ?? ['*'];
|
||||
if (includeTableName) f = f.map((s) => '$tableName.$s').toList();
|
||||
b.write(f.join(', '));
|
||||
b.write(' FROM $tableName');
|
||||
var whereClause = where.compile();
|
||||
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(' ${item.compile()}');
|
||||
if (_crossJoin != null) b.write(' CROSS JOIN $_crossJoin');
|
||||
if (_join != null) b.write(' ${_join.compile()}');
|
||||
return b.toString();
|
||||
}
|
||||
}
|
||||
|
@ -57,9 +138,10 @@ abstract class Query<T, Where extends QueryWhere> extends QueryBase<T> {
|
|||
/// Builds a SQL `WHERE` clause.
|
||||
abstract class QueryWhere {
|
||||
final Set<QueryWhere> _and = new Set();
|
||||
final Set<QueryWhere> _not = new Set();
|
||||
final Set<QueryWhere> _or = new Set();
|
||||
|
||||
Map<String, SqlExpressionBuilder> get expressionBuilders;
|
||||
Iterable<SqlExpressionBuilder> get expressionBuilders;
|
||||
|
||||
void and(QueryWhere other) {
|
||||
_and.add(other);
|
||||
|
@ -69,12 +151,13 @@ abstract class QueryWhere {
|
|||
_or.add(other);
|
||||
}
|
||||
|
||||
String compile() {
|
||||
String compile({String tableName}) {
|
||||
var b = new StringBuffer();
|
||||
int i = 0;
|
||||
|
||||
for (var entry in expressionBuilders.entries) {
|
||||
var key = entry.key, builder = entry.value;
|
||||
for (var builder in expressionBuilders) {
|
||||
var key = builder.columnName;
|
||||
if (tableName != null) key = '$tableName.$key';
|
||||
if (builder.hasValue) {
|
||||
if (i++ > 0) b.write(' AND ');
|
||||
b.write('$key ${builder.compile()}');
|
||||
|
@ -86,6 +169,11 @@ abstract class QueryWhere {
|
|||
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');
|
||||
|
@ -106,12 +194,55 @@ class Union<T> extends QueryBase<T> {
|
|||
T deserialize(List row) => left.deserialize(row);
|
||||
|
||||
@override
|
||||
String compile() {
|
||||
String compile({bool includeTableName: false}) {
|
||||
var selector = all == true ? 'UNION ALL' : 'UNION';
|
||||
return '(${left.compile()}) $selector (${right.compile()})';
|
||||
return '(${left.compile(includeTableName: includeTableName)}) $selector (${right.compile(includeTableName: includeTableName)})';
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a SQL `JOIN` query.
|
||||
class Join {
|
||||
final JoinType type;
|
||||
final Query from;
|
||||
final String to, key, value, op;
|
||||
|
||||
Join(this.type, this.from, this.to, this.key, this.value, {this.op: '='});
|
||||
|
||||
String compile() {
|
||||
var b = new StringBuffer();
|
||||
var left = '${from.tableName}.$key';
|
||||
var right = '$to.$value';
|
||||
|
||||
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(' $to ON $left$op$right');
|
||||
return b.toString();
|
||||
}
|
||||
}
|
||||
|
||||
class JoinOn {
|
||||
final SqlExpressionBuilder key;
|
||||
final SqlExpressionBuilder value;
|
||||
|
||||
JoinOn(this.key, this.value);
|
||||
}
|
||||
|
||||
/// An abstract interface that performs queries.
|
||||
///
|
||||
/// This class should be implemented.
|
||||
|
|
Loading…
Reference in a new issue