From 34d949d0e20666f1e5493e3b231618a4daf7f3fb Mon Sep 17 00:00:00 2001 From: Tobe O Date: Sat, 1 Dec 2018 13:23:50 -0500 Subject: [PATCH] Add joins --- angel_orm/example/main.dart | 23 ++--- angel_orm/lib/src/annotations.dart | 2 +- angel_orm/lib/src/builder.dart | 23 +++-- angel_orm/lib/src/query.dart | 157 ++++++++++++++++++++++++++--- 4 files changed, 172 insertions(+), 33 deletions(-) diff --git a/angel_orm/example/main.dart b/angel_orm/example/main.dart index 75f76a0f..2984d734 100644 --- a/angel_orm/example/main.dart +++ b/angel_orm/example/main.dart @@ -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 { class EmployeeQueryWhere extends QueryWhere { @override - Map get expressionBuilders { - return { - 'id': id, - 'first_name': firstName, - 'last_name': lastName, - 'salary': salary, - 'created_at': createdAt, - 'updated_at': updatedAt - }; + Iterable get expressionBuilders { + return [id, firstName, lastName, salary, createdAt, updatedAt]; } final NumericSqlExpressionBuilder id = - new NumericSqlExpressionBuilder(); + new NumericSqlExpressionBuilder('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 salary = - new NumericSqlExpressionBuilder(); + new NumericSqlExpressionBuilder('salary'); final DateTimeSqlExpressionBuilder createdAt = new DateTimeSqlExpressionBuilder('created_at'); diff --git a/angel_orm/lib/src/annotations.dart b/angel_orm/lib/src/annotations.dart index 5d784e07..d2fefebb 100644 --- a/angel_orm/lib/src/annotations.dart +++ b/angel_orm/lib/src/annotations.dart @@ -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 } diff --git a/angel_orm/lib/src/builder.dart b/angel_orm/lib/src/builder.dart index 6fbe6547..6d809f19 100644 --- a/angel_orm/lib/src/builder.dart +++ b/angel_orm/lib/src/builder.dart @@ -28,6 +28,8 @@ String sanitizeExpression(String unsafe) { } abstract class SqlExpressionBuilder { + String get columnName; + bool get hasValue; String compile(); @@ -43,11 +45,14 @@ abstract class SqlExpressionBuilder { class NumericSqlExpressionBuilder implements SqlExpressionBuilder { + 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 } class StringSqlExpressionBuilder implements SqlExpressionBuilder { + 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 { } class BooleanSqlExpressionBuilder implements SqlExpressionBuilder { + 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 { class DateTimeSqlExpressionBuilder implements SqlExpressionBuilder { final NumericSqlExpressionBuilder year = - new NumericSqlExpressionBuilder(), - month = new NumericSqlExpressionBuilder(), - day = new NumericSqlExpressionBuilder(), - hour = new NumericSqlExpressionBuilder(), - minute = new NumericSqlExpressionBuilder(), - second = new NumericSqlExpressionBuilder(); + new NumericSqlExpressionBuilder('year'), + month = new NumericSqlExpressionBuilder('month'), + day = new NumericSqlExpressionBuilder('day'), + hour = new NumericSqlExpressionBuilder('hour'), + minute = new NumericSqlExpressionBuilder('minute'), + second = new NumericSqlExpressionBuilder('second'); final String columnName; String _raw; diff --git a/angel_orm/lib/src/query.dart b/angel_orm/lib/src/query.dart index cfb23413..d679ac57 100644 --- a/angel_orm/lib/src/query.dart +++ b/angel_orm/lib/src/query.dart @@ -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 { - String compile(); + String compile({bool includeTableName: false}); T deserialize(List row); @@ -25,8 +26,22 @@ abstract class QueryBase { } } +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 extends QueryBase { + final List _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 extends QueryBase { /// 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 extends QueryBase { /// Builds a SQL `WHERE` clause. abstract class QueryWhere { final Set _and = new Set(); + final Set _not = new Set(); final Set _or = new Set(); - Map get expressionBuilders; + Iterable 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 extends QueryBase { 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.