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')
|
..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');
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
Loading…
Reference in a new issue