Add joins

This commit is contained in:
Tobe O 2018-12-01 13:23:50 -05:00
parent cb73f4a112
commit 34d949d0e2
4 changed files with 172 additions and 33 deletions

View file

@ -11,6 +11,8 @@ main() async {
..lastName.equals('Person') ..lastName.equals('Person')
..or(new EmployeeQueryWhere()..salary.greaterThanOrEqualTo(75000)); ..or(new EmployeeQueryWhere()..salary.greaterThanOrEqualTo(75000));
query.join('companies', 'company_id', 'id');
var richPerson = await query.getOne(new _FakeExecutor()); var richPerson = await query.getOne(new _FakeExecutor());
print(richPerson.toJson()); print(richPerson.toJson());
} }
@ -63,26 +65,21 @@ class EmployeeQuery extends Query<Employee, EmployeeQueryWhere> {
class EmployeeQueryWhere extends QueryWhere { class EmployeeQueryWhere extends QueryWhere {
@override @override
Map<String, SqlExpressionBuilder> get expressionBuilders { Iterable<SqlExpressionBuilder> get expressionBuilders {
return { return [id, firstName, lastName, salary, createdAt, updatedAt];
'id': id,
'first_name': firstName,
'last_name': lastName,
'salary': salary,
'created_at': createdAt,
'updated_at': updatedAt
};
} }
final NumericSqlExpressionBuilder<int> id = 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 = final NumericSqlExpressionBuilder<double> salary =
new NumericSqlExpressionBuilder<double>(); new NumericSqlExpressionBuilder<double>('salary');
final DateTimeSqlExpressionBuilder createdAt = final DateTimeSqlExpressionBuilder createdAt =
new DateTimeSqlExpressionBuilder('created_at'); new DateTimeSqlExpressionBuilder('created_at');

View file

@ -15,4 +15,4 @@ class CanJoin {
} }
/// The various types of [Join]. /// The various types of [Join].
enum JoinType { join, left, right, full, self } enum JoinType { inner, left, right, full, self }

View file

@ -28,6 +28,8 @@ String sanitizeExpression(String unsafe) {
} }
abstract class SqlExpressionBuilder<T> { abstract class SqlExpressionBuilder<T> {
String get columnName;
bool get hasValue; bool get hasValue;
String compile(); String compile();
@ -43,11 +45,14 @@ abstract class SqlExpressionBuilder<T> {
class NumericSqlExpressionBuilder<T extends num> class NumericSqlExpressionBuilder<T extends num>
implements SqlExpressionBuilder<T> { implements SqlExpressionBuilder<T> {
final String columnName;
bool _hasValue = false; bool _hasValue = false;
String _op = '='; String _op = '=';
String _raw; String _raw;
T _value; T _value;
NumericSqlExpressionBuilder(this.columnName);
@override @override
bool get hasValue => _hasValue; bool get hasValue => _hasValue;
@ -123,9 +128,12 @@ class NumericSqlExpressionBuilder<T extends num>
} }
class StringSqlExpressionBuilder implements SqlExpressionBuilder<String> { class StringSqlExpressionBuilder implements SqlExpressionBuilder<String> {
final String columnName;
bool _hasValue = false; bool _hasValue = false;
String _op = '=', _raw, _value; String _op = '=', _raw, _value;
StringSqlExpressionBuilder(this.columnName);
@override @override
bool get hasValue => _hasValue; bool get hasValue => _hasValue;
@ -190,10 +198,13 @@ class StringSqlExpressionBuilder implements SqlExpressionBuilder<String> {
} }
class BooleanSqlExpressionBuilder implements SqlExpressionBuilder<bool> { class BooleanSqlExpressionBuilder implements SqlExpressionBuilder<bool> {
final String columnName;
bool _hasValue = false; bool _hasValue = false;
String _op = '=', _raw; String _op = '=', _raw;
bool _value; bool _value;
BooleanSqlExpressionBuilder(this.columnName);
@override @override
bool get hasValue => _hasValue; bool get hasValue => _hasValue;
@ -243,12 +254,12 @@ class BooleanSqlExpressionBuilder implements SqlExpressionBuilder<bool> {
class DateTimeSqlExpressionBuilder implements SqlExpressionBuilder<DateTime> { class DateTimeSqlExpressionBuilder implements SqlExpressionBuilder<DateTime> {
final NumericSqlExpressionBuilder<int> year = final NumericSqlExpressionBuilder<int> year =
new NumericSqlExpressionBuilder<int>(), new NumericSqlExpressionBuilder<int>('year'),
month = new NumericSqlExpressionBuilder<int>(), month = new NumericSqlExpressionBuilder<int>('month'),
day = new NumericSqlExpressionBuilder<int>(), day = new NumericSqlExpressionBuilder<int>('day'),
hour = new NumericSqlExpressionBuilder<int>(), hour = new NumericSqlExpressionBuilder<int>('hour'),
minute = new NumericSqlExpressionBuilder<int>(), minute = new NumericSqlExpressionBuilder<int>('minute'),
second = new NumericSqlExpressionBuilder<int>(); second = new NumericSqlExpressionBuilder<int>('second');
final String columnName; final String columnName;
String _raw; String _raw;

View file

@ -1,9 +1,10 @@
import 'dart:async'; import 'dart:async';
import 'annotations.dart';
import 'builder.dart'; import 'builder.dart';
/// A base class for objects that compile to SQL queries, typically within an ORM. /// A base class for objects that compile to SQL queries, typically within an ORM.
abstract class QueryBase<T> { abstract class QueryBase<T> {
String compile(); String compile({bool includeTableName: false});
T deserialize(List row); 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. /// A SQL `SELECT` query builder.
abstract class Query<T, Where extends QueryWhere> extends QueryBase<T> { 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. /// The table against which to execute this query.
String get tableName; String get tableName;
@ -40,16 +55,82 @@ abstract class Query<T, Where extends QueryWhere> extends QueryBase<T> {
/// This is often a generated class. /// This is often a generated class.
Where get where; 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 @override
String compile() { String compile({bool includeTableName: false}) {
var b = new StringBuffer('SELECT '); var b = new StringBuffer('SELECT ');
if (fields == null) var f = fields ?? ['*'];
b.write('*'); if (includeTableName) f = f.map((s) => '$tableName.$s').toList();
else b.write(f.join(', '));
b.write(fields.join(', '));
b.write(' FROM $tableName'); b.write(' FROM $tableName');
var whereClause = where.compile(); var whereClause =
where.compile(tableName: includeTableName ? tableName : null);
if (whereClause.isNotEmpty) b.write(' WHERE $whereClause'); 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(); return b.toString();
} }
} }
@ -57,9 +138,10 @@ abstract class Query<T, Where extends QueryWhere> extends QueryBase<T> {
/// Builds a SQL `WHERE` clause. /// Builds a SQL `WHERE` clause.
abstract class QueryWhere { abstract class QueryWhere {
final Set<QueryWhere> _and = new Set(); final Set<QueryWhere> _and = new Set();
final Set<QueryWhere> _not = new Set();
final Set<QueryWhere> _or = new Set(); final Set<QueryWhere> _or = new Set();
Map<String, SqlExpressionBuilder> get expressionBuilders; Iterable<SqlExpressionBuilder> get expressionBuilders;
void and(QueryWhere other) { void and(QueryWhere other) {
_and.add(other); _and.add(other);
@ -69,12 +151,13 @@ abstract class QueryWhere {
_or.add(other); _or.add(other);
} }
String compile() { String compile({String tableName}) {
var b = new StringBuffer(); var b = new StringBuffer();
int i = 0; int i = 0;
for (var entry in expressionBuilders.entries) { for (var builder in expressionBuilders) {
var key = entry.key, builder = entry.value; var key = builder.columnName;
if (tableName != null) key = '$tableName.$key';
if (builder.hasValue) { if (builder.hasValue) {
if (i++ > 0) b.write(' AND '); if (i++ > 0) b.write(' AND ');
b.write('$key ${builder.compile()}'); b.write('$key ${builder.compile()}');
@ -86,6 +169,11 @@ abstract class QueryWhere {
if (sql.isNotEmpty) b.write(' AND $sql'); 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) { for (var other in _or) {
var sql = other.compile(); var sql = other.compile();
if (sql.isNotEmpty) b.write(' OR $sql'); if (sql.isNotEmpty) b.write(' OR $sql');
@ -106,12 +194,55 @@ class Union<T> extends QueryBase<T> {
T deserialize(List row) => left.deserialize(row); T deserialize(List row) => left.deserialize(row);
@override @override
String compile() { String compile({bool includeTableName: false}) {
var selector = all == true ? 'UNION ALL' : 'UNION'; 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. /// An abstract interface that performs queries.
/// ///
/// This class should be implemented. /// This class should be implemented.