diff --git a/angel_orm/CHANGELOG.md b/angel_orm/CHANGELOG.md index 54a6bd95..cc359b6c 100644 --- a/angel_orm/CHANGELOG.md +++ b/angel_orm/CHANGELOG.md @@ -1,3 +1,15 @@ +# 2.1.0-beta +* Split the formerly 600+ line `src/query.dart` up into +separate files. +* **BREAKING**: Add a required `QueryExecutor` argument to `transaction` +callbacks. +* Make `JoinBuilder` take `to` as a `String Function()`. This will allow +ORM queries to reference their joined subqueries. +* Removed deprecated `Join`, `toSql`, `sanitizeExpression`, `isAscii`. +* Always put `ORDER BY` before `LIMIT`. +* `and`, `or`, `not` in `QueryWhere` include parentheses. +* Add `joinType` to `Relationship` class. + # 2.0.2 * Place `LIMIT` and `OFFSET` after `ORDER BY`. diff --git a/angel_orm/example/main.dart b/angel_orm/example/main.dart index 719500f6..fbf6866d 100644 --- a/angel_orm/example/main.dart +++ b/angel_orm/example/main.dart @@ -34,7 +34,7 @@ class _FakeExecutor extends QueryExecutor { } @override - Future transaction(FutureOr Function() f) { + Future transaction(FutureOr Function(QueryExecutor) f) { throw UnsupportedError('Transactions are not supported.'); } } diff --git a/angel_orm/lib/angel_orm.dart b/angel_orm/lib/angel_orm.dart index b7931d80..91952278 100644 --- a/angel_orm/lib/angel_orm.dart +++ b/angel_orm/lib/angel_orm.dart @@ -1,5 +1,15 @@ export 'src/annotations.dart'; export 'src/builder.dart'; +export 'src/join_builder.dart'; +export 'src/join_on.dart'; +export 'src/map_query_values.dart'; export 'src/migration.dart'; -export 'src/relations.dart'; +export 'src/order_by.dart'; +export 'src/query_base.dart'; +export 'src/query_executor.dart'; +export 'src/query_values.dart'; +export 'src/query_where.dart'; export 'src/query.dart'; +export 'src/relations.dart'; +export 'src/union.dart'; +export 'src/util.dart'; diff --git a/angel_orm/lib/src/annotations.dart b/angel_orm/lib/src/annotations.dart index 4bc7147a..1c06f020 100644 --- a/angel_orm/lib/src/annotations.dart +++ b/angel_orm/lib/src/annotations.dart @@ -27,14 +27,5 @@ class Orm { const Orm({this.tableName, this.generateMigrations = true}); } -@deprecated -class Join { - final Type against; - final String foreignKey; - final JoinType type; - - const Join(this.against, this.foreignKey, {this.type = JoinType.inner}); -} - /// The various types of join. enum JoinType { inner, left, right, full, self } diff --git a/angel_orm/lib/src/builder.dart b/angel_orm/lib/src/builder.dart index eac463d5..113f76f3 100644 --- a/angel_orm/lib/src/builder.dart +++ b/angel_orm/lib/src/builder.dart @@ -1,41 +1,10 @@ import 'dart:convert'; - -import 'package:charcode/ascii.dart'; import 'package:intl/intl.dart' show DateFormat; -import 'package:string_scanner/string_scanner.dart'; import 'query.dart'; final DateFormat dateYmd = DateFormat('yyyy-MM-dd'); final DateFormat dateYmdHms = DateFormat('yyyy-MM-dd HH:mm:ss'); -/// The ORM prefers using substitution values, which allow for prepared queries, -/// and prevent SQL injection attacks. -@deprecated -String sanitizeExpression(String unsafe) { - var buf = StringBuffer(); - var scanner = StringScanner(unsafe); - int ch; - - while (!scanner.isDone) { - // Ignore comment starts - if (scanner.scan('--') || scanner.scan('/*')) { - continue; - } - - // Ignore all single quotes and attempted escape sequences - else if (scanner.scan("'") || scanner.scan('\\')) { - continue; - } - - // Otherwise, add the next char, unless it's a null byte. - else if ((ch = scanner.readChar()) != $nul && ch != null) { - buf.writeCharCode(ch); - } - } - - return toSql(buf.toString(), withQuotes: false); -} - abstract class SqlExpressionBuilder { final Query query; final String columnName; diff --git a/angel_orm/lib/src/join_builder.dart b/angel_orm/lib/src/join_builder.dart new file mode 100644 index 00000000..bf5836dd --- /dev/null +++ b/angel_orm/lib/src/join_builder.dart @@ -0,0 +1,67 @@ +import 'annotations.dart'; +import 'query.dart'; + +/// Builds a SQL `JOIN` query. +class JoinBuilder { + final JoinType type; + final Query from; + final String key, value, op, alias; + + /// A callback to produces the expression to join against, i.e. + /// a table name, or the result of compiling a query. + final String Function() to; + final List additionalFields; + + JoinBuilder(this.type, this.from, this.to, this.key, this.value, + {this.op = '=', this.alias, this.additionalFields = const []}) { + assert(to != null, + 'computation of this join threw an error, and returned null.'); + } + + String get fieldName { + var right = '$to.$value'; + if (alias != null) right = '$alias.$value'; + return right; + } + + String nameFor(String name) { + var right = '$to.$name'; + if (alias != null) right = '$alias.$name'; + return right; + } + + String compile(Set trampoline) { + var compiledTo = to(); + if (compiledTo == null) { + print( + 'NULLLLL $to; from $from; key: $key, value: $value, addl: $additionalFields'); + } + if (compiledTo == null) return null; + var b = StringBuffer(); + var left = '${from.tableName}.$key'; + var right = fieldName; + + 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(' $compiledTo'); + if (alias != null) b.write(' $alias'); + b.write(' ON $left$op$right'); + return b.toString(); + } +} diff --git a/angel_orm/lib/src/join_on.dart b/angel_orm/lib/src/join_on.dart new file mode 100644 index 00000000..7bfed401 --- /dev/null +++ b/angel_orm/lib/src/join_on.dart @@ -0,0 +1,8 @@ +import 'builder.dart'; + +class JoinOn { + final SqlExpressionBuilder key; + final SqlExpressionBuilder value; + + JoinOn(this.key, this.value); +} diff --git a/angel_orm/lib/src/map_query_values.dart b/angel_orm/lib/src/map_query_values.dart new file mode 100644 index 00000000..398ba9bf --- /dev/null +++ b/angel_orm/lib/src/map_query_values.dart @@ -0,0 +1,9 @@ +import 'query_values.dart'; + +/// A [QueryValues] implementation that simply writes to a [Map]. +class MapQueryValues extends QueryValues { + final Map values = {}; + + @override + Map toMap() => values; +} diff --git a/angel_orm/lib/src/migration.dart b/angel_orm/lib/src/migration.dart index 159537a0..6d0dd52e 100644 --- a/angel_orm/lib/src/migration.dart +++ b/angel_orm/lib/src/migration.dart @@ -26,11 +26,18 @@ class Column { /// Specifies what kind of index this column is, if any. final IndexType indexType; + /// A custom SQL expression to execute, instead of a named column. + final String expression; + const Column( {this.isNullable = true, this.length, this.type, - this.indexType = IndexType.none}); + this.indexType = IndexType.none, + this.expression}); + + /// Returns `true` if [expression] is not `null`. + bool get hasExpression => expression != null; } class PrimaryKey extends Column { diff --git a/angel_orm/lib/src/order_by.dart b/angel_orm/lib/src/order_by.dart new file mode 100644 index 00000000..4501cf45 --- /dev/null +++ b/angel_orm/lib/src/order_by.dart @@ -0,0 +1,8 @@ +class OrderBy { + final String key; + final bool descending; + + const OrderBy(this.key, {this.descending = false}); + + String compile() => descending ? '$key DESC' : '$key ASC'; +} diff --git a/angel_orm/lib/src/query.dart b/angel_orm/lib/src/query.dart index 96e38c96..ce066897 100644 --- a/angel_orm/lib/src/query.dart +++ b/angel_orm/lib/src/query.dart @@ -1,115 +1,11 @@ import 'dart:async'; -import 'package:charcode/ascii.dart'; import 'annotations.dart'; -import 'builder.dart'; - -bool isAscii(int ch) => ch >= $nul && ch <= $del; - -/// A base class for objects that compile to SQL queries, typically within an ORM. -abstract class QueryBase { - /// Casts to perform when querying the database. - Map get casts => {}; - - /// Values to insert into a prepared statement. - final Map substitutionValues = {}; - - /// The table against which to execute this query. - String get tableName; - - /// The list of fields returned by this query. - /// - /// If it's `null`, then this query will perform a `SELECT *`. - List get fields; - - /// A String of all [fields], joined by a comma (`,`). - String get fieldSet => fields.map((k) { - var cast = casts[k]; - return cast == null ? k : 'CAST ($k AS $cast)'; - }).join(', '); - - String compile(Set trampoline, - {bool includeTableName = false, String preamble, bool withFields = true}); - - T deserialize(List row); - - Future> get(QueryExecutor executor) async { - var sql = compile(Set()); - return executor - .query(tableName, sql, substitutionValues) - .then((it) => it.map(deserialize).toList()); - } - - Future getOne(QueryExecutor executor) { - return get(executor).then((it) => it.isEmpty ? null : it.first); - } - - Union union(QueryBase other) { - return Union(this, other); - } - - Union unionAll(QueryBase other) { - return Union(this, other, all: true); - } -} - -class OrderBy { - final String key; - final bool descending; - - const OrderBy(this.key, {this.descending = false}); - - String compile() => descending ? '$key DESC' : '$key ASC'; -} - -/// The ORM prefers using substitution values, which allow for prepared queries, -/// and prevent SQL injection attacks. -@deprecated -String toSql(Object obj, {bool withQuotes = true}) { - if (obj is DateTime) { - return withQuotes ? "'${dateYmdHms.format(obj)}'" : dateYmdHms.format(obj); - } else if (obj is bool) { - return obj ? 'TRUE' : 'FALSE'; - } else if (obj == null) { - return 'NULL'; - } else if (obj is String) { - var b = StringBuffer(); - var escaped = false; - var it = obj.runes.iterator; - - while (it.moveNext()) { - if (it.current == $nul) { - continue; // Skip null byte - } else if (it.current == $single_quote) { - escaped = true; - b.write('\\x'); - b.write(it.current.toRadixString(16).padLeft(2, '0')); - } else if (isAscii(it.current)) { - b.writeCharCode(it.current); - } else if (it.currentSize == 1) { - escaped = true; - b.write('\\u'); - b.write(it.current.toRadixString(16).padLeft(4, '0')); - } else if (it.currentSize == 2) { - escaped = true; - b.write('\\U'); - b.write(it.current.toRadixString(16).padLeft(8, '0')); - } else { - throw UnsupportedError( - 'toSql() cannot encode a rune of size (${it.currentSize})'); - } - } - - if (!withQuotes) { - return b.toString(); - } else if (escaped) { - return "E'$b'"; - } else { - return "'$b'"; - } - } else { - return obj.toString(); - } -} +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 { @@ -117,9 +13,18 @@ abstract class Query extends QueryBase { final Map _names = {}; final List _orderBy = []; + // An optional "parent query". If provided, [reserveName] will operate in + // the parent's context. + final Query parent; + String _crossJoin, _groupBy; int _limit, _offset; + Query({this.parent}); + + Map get substitutionValues => + parent?.substitutionValues ?? super.substitutionValues; + /// A reference to an abstract query builder. /// /// This is usually a generated class. @@ -136,6 +41,7 @@ abstract class Query extends QueryBase { /// Returns a unique version of [name], which will not produce a collision within /// the context of this [query]. String reserveName(String name) { + if (parent != null) return parent.reserveName(name); var n = _names[name] ??= 0; _names[name]++; return n == 0 ? name : '${name}$n'; @@ -211,13 +117,15 @@ abstract class Query extends QueryBase { } } - String _compileJoin(tableName, Set trampoline) { + String Function() _compileJoin(tableName, Set trampoline) { if (tableName is String) { - return tableName; + return () => tableName; } else if (tableName is Query) { - var c = tableName.compile(trampoline); - if (c == null) return c; - return '($c)'; + return () { + var c = tableName.compile(trampoline); + if (c == null) return c; + return '($c)'; + }; } else { throw ArgumentError.value( tableName, 'tableName', 'must be a String or Query'); @@ -311,6 +219,8 @@ abstract class Query extends QueryBase { b.write(' '); List f; + var compiledJoins = {}; + if (fields == null) { f = ['*']; } else { @@ -321,10 +231,16 @@ abstract class Query extends QueryBase { 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); + var c = compiledJoins[j] = j.compile(trampoline); + if (c != null) { + var additional = j.additionalFields.map(j.nameFor).toList(); + f.addAll(additional); + } else { + // If compilation failed, fill in NULL placeholders. + for (var i = 0; i < j.additionalFields.length; i++) { + f.add('NULL'); + } + } }); } if (withFields) b.write(f.join(', ')); @@ -335,7 +251,7 @@ abstract class Query extends QueryBase { if (preamble == null) { if (_crossJoin != null) b.write(' CROSS JOIN $_crossJoin'); for (var join in _joins) { - var c = join.compile(trampoline); + var c = compiledJoins[join]; if (c != null) b.write(' $c'); } } @@ -367,11 +283,11 @@ abstract class Query extends QueryBase { fields.map(adornWithTableName).toList()) .then((it) => it.map(deserialize).toList()); } else { - return executor.transaction(() async { + return executor.transaction((tx) async { // TODO: Can this be done with just *one* query? - var existing = await get(executor); + var existing = await get(tx); //var sql = compile(preamble: 'SELECT $tableName.id', withFields: false); - return executor + return tx .query(tableName, sql, substitutionValues) .then((_) => existing); }); @@ -424,241 +340,3 @@ abstract class Query extends QueryBase { return update(executor).then((it) => it.isEmpty ? null : it.first); } } - -abstract class QueryValues { - Map get casts => {}; - - Map toMap(); - - String applyCast(String name, String sub) { - if (casts.containsKey(name)) { - var type = casts[name]; - return 'CAST ($sub as $type)'; - } else { - return sub; - } - } - - String compileInsert(Query query, String tableName) { - var data = Map.from(toMap()); - var keys = data.keys.toList(); - keys.where((k) => !query.fields.contains(k)).forEach(data.remove); - if (data.isEmpty) return null; - - var fieldSet = data.keys.join(', '); - var b = StringBuffer('INSERT INTO $tableName ($fieldSet) VALUES ('); - int i = 0; - - for (var entry in data.entries) { - if (i++ > 0) b.write(', '); - - var name = query.reserveName(entry.key); - var s = applyCast(entry.key, '@$name'); - query.substitutionValues[name] = entry.value; - b.write(s); - } - - b.write(')'); - return b.toString(); - } - - String compileForUpdate(Query query) { - var data = toMap(); - if (data.isEmpty) return null; - var b = StringBuffer('SET'); - int i = 0; - - for (var entry in data.entries) { - if (i++ > 0) b.write(','); - b.write(' '); - b.write(entry.key); - b.write('='); - - var name = query.reserveName(entry.key); - var s = applyCast(entry.key, '@$name'); - query.substitutionValues[name] = entry.value; - b.write(s); - } - return b.toString(); - } -} - -/// A [QueryValues] implementation that simply writes to a [Map]. -class MapQueryValues extends QueryValues { - final Map values = {}; - - @override - Map toMap() => values; -} - -/// Builds a SQL `WHERE` clause. -abstract class QueryWhere { - final Set _and = Set(); - final Set _not = Set(); - final Set _or = Set(); - - Iterable get expressionBuilders; - - void and(QueryWhere other) { - _and.add(other); - } - - void not(QueryWhere other) { - _not.add(other); - } - - void or(QueryWhere other) { - _or.add(other); - } - - String compile({String tableName}) { - var b = StringBuffer(); - int i = 0; - - for (var builder in expressionBuilders) { - var key = builder.columnName; - if (tableName != null) key = '$tableName.$key'; - if (builder.hasValue) { - if (i++ > 0) b.write(' AND '); - if (builder is DateTimeSqlExpressionBuilder || - (builder is JsonSqlExpressionBuilder && builder.hasRaw)) { - if (tableName != null) b.write('$tableName.'); - b.write(builder.compile()); - } else { - b.write('$key ${builder.compile()}'); - } - } - } - - for (var other in _and) { - var sql = other.compile(); - 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'); - } - - return b.toString(); - } -} - -/// Represents the `UNION` of two subqueries. -class Union extends QueryBase { - /// The subject(s) of this binary operation. - final QueryBase left, right; - - /// Whether this is a `UNION ALL` operation. - final bool all; - - @override - final String tableName; - - Union(this.left, this.right, {this.all = false, String tableName}) - : this.tableName = tableName ?? left.tableName { - substitutionValues - ..addAll(left.substitutionValues) - ..addAll(right.substitutionValues); - } - - @override - List get fields => left.fields; - - @override - T deserialize(List row) => left.deserialize(row); - - @override - String compile(Set trampoline, - {bool includeTableName = false, - String preamble, - bool withFields = true}) { - var selector = all == true ? 'UNION ALL' : 'UNION'; - var t1 = Set.from(trampoline); - var t2 = Set.from(trampoline); - return '(${left.compile(t1, includeTableName: includeTableName)}) $selector (${right.compile(t2, includeTableName: includeTableName)})'; - } -} - -/// Builds a SQL `JOIN` query. -class JoinBuilder { - final JoinType type; - final Query from; - final String to, key, value, op, alias; - final List additionalFields; - - JoinBuilder(this.type, this.from, this.to, this.key, this.value, - {this.op = '=', this.alias, this.additionalFields = const []}) { - assert(to != null, - 'computation of this join threw an error, and returned null.'); - } - - String get fieldName { - var right = '$to.$value'; - if (alias != null) right = '$alias.$value'; - return right; - } - - String nameFor(String name) { - var right = '$to.$name'; - if (alias != null) right = '$alias.$name'; - return right; - } - - String compile(Set trampoline) { - if (to == null) return null; - var b = StringBuffer(); - var left = '${from.tableName}.$key'; - var right = fieldName; - - 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'); - if (alias != null) b.write(' $alias'); - b.write(' 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. -abstract class QueryExecutor { - const QueryExecutor(); - - /// Executes a single query. - Future> query( - String tableName, String query, Map substitutionValues, - [List returningFields]); - - /// Begins a database transaction. - Future transaction(FutureOr f()); -} diff --git a/angel_orm/lib/src/query_base.dart b/angel_orm/lib/src/query_base.dart new file mode 100644 index 00000000..8e378636 --- /dev/null +++ b/angel_orm/lib/src/query_base.dart @@ -0,0 +1,50 @@ +import 'dart:async'; +import 'query_executor.dart'; +import 'union.dart'; + +/// A base class for objects that compile to SQL queries, typically within an ORM. +abstract class QueryBase { + /// Casts to perform when querying the database. + Map get casts => {}; + + /// Values to insert into a prepared statement. + final Map substitutionValues = {}; + + /// The table against which to execute this query. + String get tableName; + + /// The list of fields returned by this query. + /// + /// If it's `null`, then this query will perform a `SELECT *`. + List get fields; + + /// A String of all [fields], joined by a comma (`,`). + String get fieldSet => fields.map((k) { + var cast = casts[k]; + return cast == null ? k : 'CAST ($k AS $cast)'; + }).join(', '); + + String compile(Set trampoline, + {bool includeTableName = false, String preamble, bool withFields = true}); + + T deserialize(List row); + + Future> get(QueryExecutor executor) async { + var sql = compile(Set()); + return executor + .query(tableName, sql, substitutionValues) + .then((it) => it.map(deserialize).toList()); + } + + Future getOne(QueryExecutor executor) { + return get(executor).then((it) => it.isEmpty ? null : it.first); + } + + Union union(QueryBase other) { + return Union(this, other); + } + + Union unionAll(QueryBase other) { + return Union(this, other, all: true); + } +} diff --git a/angel_orm/lib/src/query_executor.dart b/angel_orm/lib/src/query_executor.dart new file mode 100644 index 00000000..85474365 --- /dev/null +++ b/angel_orm/lib/src/query_executor.dart @@ -0,0 +1,23 @@ +import 'dart:async'; + +/// An abstract interface that performs queries. +/// +/// This class should be implemented. +abstract class QueryExecutor { + const QueryExecutor(); + + /// Executes a single query. + Future> query( + String tableName, String query, Map substitutionValues, + [List returningFields]); + + /// Enters a database transaction, performing the actions within, + /// and returning the results of [f]. + /// + /// If [f] fails, the transaction will be rolled back, and the + /// responsible exception will be re-thrown. + /// + /// Whether nested transactions are supported depends on the + /// underlying driver. + Future transaction(FutureOr Function(QueryExecutor) f); +} diff --git a/angel_orm/lib/src/query_values.dart b/angel_orm/lib/src/query_values.dart new file mode 100644 index 00000000..368cc8c6 --- /dev/null +++ b/angel_orm/lib/src/query_values.dart @@ -0,0 +1,59 @@ +import 'query.dart'; + +abstract class QueryValues { + Map get casts => {}; + + Map toMap(); + + String applyCast(String name, String sub) { + if (casts.containsKey(name)) { + var type = casts[name]; + return 'CAST ($sub as $type)'; + } else { + return sub; + } + } + + String compileInsert(Query query, String tableName) { + var data = Map.from(toMap()); + var keys = data.keys.toList(); + keys.where((k) => !query.fields.contains(k)).forEach(data.remove); + if (data.isEmpty) return null; + + var fieldSet = data.keys.join(', '); + var b = StringBuffer('INSERT INTO $tableName ($fieldSet) VALUES ('); + int i = 0; + + for (var entry in data.entries) { + if (i++ > 0) b.write(', '); + + var name = query.reserveName(entry.key); + var s = applyCast(entry.key, '@$name'); + query.substitutionValues[name] = entry.value; + b.write(s); + } + + b.write(')'); + return b.toString(); + } + + String compileForUpdate(Query query) { + var data = toMap(); + if (data.isEmpty) return null; + var b = StringBuffer('SET'); + int i = 0; + + for (var entry in data.entries) { + if (i++ > 0) b.write(','); + b.write(' '); + b.write(entry.key); + b.write('='); + + var name = query.reserveName(entry.key); + var s = applyCast(entry.key, '@$name'); + query.substitutionValues[name] = entry.value; + b.write(s); + } + return b.toString(); + } +} diff --git a/angel_orm/lib/src/query_where.dart b/angel_orm/lib/src/query_where.dart new file mode 100644 index 00000000..ebe6dd7b --- /dev/null +++ b/angel_orm/lib/src/query_where.dart @@ -0,0 +1,59 @@ +import 'builder.dart'; + +/// Builds a SQL `WHERE` clause. +abstract class QueryWhere { + final Set _and = Set(); + final Set _not = Set(); + final Set _or = Set(); + + Iterable get expressionBuilders; + + void and(QueryWhere other) { + _and.add(other); + } + + void not(QueryWhere other) { + _not.add(other); + } + + void or(QueryWhere other) { + _or.add(other); + } + + String compile({String tableName}) { + var b = StringBuffer(); + int i = 0; + + for (var builder in expressionBuilders) { + var key = builder.columnName; + if (tableName != null) key = '$tableName.$key'; + if (builder.hasValue) { + if (i++ > 0) b.write(' AND '); + if (builder is DateTimeSqlExpressionBuilder || + (builder is JsonSqlExpressionBuilder && builder.hasRaw)) { + if (tableName != null) b.write('$tableName.'); + b.write(builder.compile()); + } else { + b.write('$key ${builder.compile()}'); + } + } + } + + for (var other in _and) { + var sql = other.compile(); + 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)'); + } + + return b.toString(); + } +} diff --git a/angel_orm/lib/src/relations.dart b/angel_orm/lib/src/relations.dart index 0d736925..1ca6e357 100644 --- a/angel_orm/lib/src/relations.dart +++ b/angel_orm/lib/src/relations.dart @@ -1,3 +1,5 @@ +import 'annotations.dart'; + abstract class RelationshipType { static const int hasMany = 0; static const int hasOne = 1; @@ -11,12 +13,14 @@ class Relationship { final String foreignKey; final String foreignTable; final bool cascadeOnDelete; + final JoinType joinType; const Relationship(this.type, {this.localKey, this.foreignKey, this.foreignTable, - this.cascadeOnDelete}); + this.cascadeOnDelete, + this.joinType}); } class HasMany extends Relationship { @@ -24,12 +28,14 @@ class HasMany extends Relationship { {String localKey, String foreignKey, String foreignTable, - bool cascadeOnDelete = false}) + bool cascadeOnDelete = false, + JoinType joinType}) : super(RelationshipType.hasMany, localKey: localKey, foreignKey: foreignKey, foreignTable: foreignTable, - cascadeOnDelete: cascadeOnDelete == true); + cascadeOnDelete: cascadeOnDelete == true, + joinType: joinType); } const HasMany hasMany = HasMany(); @@ -39,22 +45,29 @@ class HasOne extends Relationship { {String localKey, String foreignKey, String foreignTable, - bool cascadeOnDelete = false}) + bool cascadeOnDelete = false, + JoinType joinType}) : super(RelationshipType.hasOne, localKey: localKey, foreignKey: foreignKey, foreignTable: foreignTable, - cascadeOnDelete: cascadeOnDelete == true); + cascadeOnDelete: cascadeOnDelete == true, + joinType: joinType); } const HasOne hasOne = HasOne(); class BelongsTo extends Relationship { - const BelongsTo({String localKey, String foreignKey, String foreignTable}) + const BelongsTo( + {String localKey, + String foreignKey, + String foreignTable, + JoinType joinType}) : super(RelationshipType.belongsTo, localKey: localKey, foreignKey: foreignKey, - foreignTable: foreignTable); + foreignTable: foreignTable, + joinType: joinType); } const BelongsTo belongsTo = BelongsTo(); @@ -66,11 +79,13 @@ class ManyToMany extends Relationship { {String localKey, String foreignKey, String foreignTable, - bool cascadeOnDelete = false}) + bool cascadeOnDelete = false, + JoinType joinType}) : super( RelationshipType.hasMany, // Many-to-Many is actually just a hasMany localKey: localKey, foreignKey: foreignKey, foreignTable: foreignTable, - cascadeOnDelete: cascadeOnDelete == true); + cascadeOnDelete: cascadeOnDelete == true, + joinType: joinType); } diff --git a/angel_orm/lib/src/union.dart b/angel_orm/lib/src/union.dart new file mode 100644 index 00000000..048ebf31 --- /dev/null +++ b/angel_orm/lib/src/union.dart @@ -0,0 +1,37 @@ +import 'query_base.dart'; + +/// Represents the `UNION` of two subqueries. +class Union extends QueryBase { + /// The subject(s) of this binary operation. + final QueryBase left, right; + + /// Whether this is a `UNION ALL` operation. + final bool all; + + @override + final String tableName; + + Union(this.left, this.right, {this.all = false, String tableName}) + : this.tableName = tableName ?? left.tableName { + substitutionValues + ..addAll(left.substitutionValues) + ..addAll(right.substitutionValues); + } + + @override + List get fields => left.fields; + + @override + T deserialize(List row) => left.deserialize(row); + + @override + String compile(Set trampoline, + {bool includeTableName = false, + String preamble, + bool withFields = true}) { + var selector = all == true ? 'UNION ALL' : 'UNION'; + var t1 = Set.from(trampoline); + var t2 = Set.from(trampoline); + return '(${left.compile(t1, includeTableName: includeTableName)}) $selector (${right.compile(t2, includeTableName: includeTableName)})'; + } +} diff --git a/angel_orm/lib/src/util.dart b/angel_orm/lib/src/util.dart new file mode 100644 index 00000000..939b36c8 --- /dev/null +++ b/angel_orm/lib/src/util.dart @@ -0,0 +1,3 @@ +import 'package:charcode/ascii.dart'; + +bool isAscii(int ch) => ch >= $nul && ch <= $del; diff --git a/angel_orm/pubspec.yaml b/angel_orm/pubspec.yaml index 6df776a7..dcd2ba3e 100644 --- a/angel_orm/pubspec.yaml +++ b/angel_orm/pubspec.yaml @@ -1,10 +1,10 @@ name: angel_orm -version: 2.0.2 +version: 2.1.0-beta description: Runtime support for Angel's ORM. Includes base classes for queries. author: Tobe O homepage: https://github.com/angel-dart/orm environment: - sdk: '>=2.0.0-dev.1.2 <3.0.0' + sdk: '>=2.0.0 <3.0.0' dependencies: charcode: ^1.0.0 intl: ^0.15.7 diff --git a/angel_orm_generator/CHANGELOG.md b/angel_orm_generator/CHANGELOG.md index a35e8481..4feb0230 100644 --- a/angel_orm_generator/CHANGELOG.md +++ b/angel_orm_generator/CHANGELOG.md @@ -1,3 +1,13 @@ +# 2.1.0-beta.1 +* `OrmBuildContext` caching is now local to a `Builder`, so `watch` +*should* finally always run when required. Should resolve +[#85](https://github.com/angel-dart/orm/issues/85). + +# 2.1.0-beta +* Relationships have always generated subqueries; now these subqueries are +available as `Query` objects on generated classes. +* Support explicitly-defined join types for relations. + # 2.0.5 * Remove `ShimFieldImpl` check, which broke relations. * Fix bug where primary key type would not be emitted in migrations. diff --git a/angel_orm_generator/analysis_options.yaml b/angel_orm_generator/analysis_options.yaml index eae1e42a..c230cee7 100644 --- a/angel_orm_generator/analysis_options.yaml +++ b/angel_orm_generator/analysis_options.yaml @@ -1,3 +1,4 @@ +include: package:pedantic/analysis_options.yaml analyzer: strong-mode: implicit-casts: false \ No newline at end of file diff --git a/angel_orm_generator/example/main.dart b/angel_orm_generator/example/main.dart index 7651e8f9..b81ac762 100644 --- a/angel_orm_generator/example/main.dart +++ b/angel_orm_generator/example/main.dart @@ -7,13 +7,13 @@ import 'package:angel_serialize/angel_serialize.dart'; part 'main.g.dart'; main() async { - var query = new EmployeeQuery() + var query = EmployeeQuery() ..where.firstName.equals('Rich') ..where.lastName.equals('Person') ..orWhere((w) => w.salary.greaterThanOrEqualTo(75000)) ..join('companies', 'company_id', 'id'); - var richPerson = await query.getOne(new _FakeExecutor()); + var richPerson = await query.getOne(_FakeExecutor()); print(richPerson.toJson()); } @@ -24,7 +24,7 @@ class _FakeExecutor extends QueryExecutor { Future> query( String tableName, String query, Map substitutionValues, [returningFields]) async { - var now = new DateTime.now(); + var now = DateTime.now(); print( '_FakeExecutor received query: $query and values: $substitutionValues'); return [ @@ -33,8 +33,8 @@ class _FakeExecutor extends QueryExecutor { } @override - Future transaction(FutureOr Function() f) { - throw new UnsupportedError('Transactions are not supported.'); + Future transaction(FutureOr Function(QueryExecutor) f) { + throw UnsupportedError('Transactions are not supported.'); } } diff --git a/angel_orm_generator/lib/src/migration_generator.dart b/angel_orm_generator/lib/src/migration_generator.dart index f5a8901a..d54f0cba 100644 --- a/angel_orm_generator/lib/src/migration_generator.dart +++ b/angel_orm_generator/lib/src/migration_generator.dart @@ -11,14 +11,14 @@ import 'package:source_gen/source_gen.dart' hide LibraryBuilder; import 'orm_build_context.dart'; Builder migrationBuilder(BuilderOptions options) { - return new SharedPartBuilder([ - new MigrationGenerator( + return SharedPartBuilder([ + MigrationGenerator( autoSnakeCaseNames: options.config['auto_snake_case_names'] != false) ], 'angel_migration'); } class MigrationGenerator extends GeneratorForAnnotation { - static final Parameter _schemaParam = new Parameter((b) => b + static final Parameter _schemaParam = Parameter((b) => b ..name = 'schema' ..type = refer('Schema')); static final Reference _schema = refer('schema'); @@ -26,13 +26,14 @@ class MigrationGenerator extends GeneratorForAnnotation { /// If `true` (default), then field names will automatically be (de)serialized as snake_case. final bool autoSnakeCaseNames; - const MigrationGenerator({this.autoSnakeCaseNames: true}); + const MigrationGenerator({this.autoSnakeCaseNames = true}); @override Future generateForAnnotatedElement( Element element, ConstantReader annotation, BuildStep buildStep) async { - if (element is! ClassElement) + if (element is! ClassElement) { throw 'Only classes can be annotated with @ORM().'; + } var generateMigrations = annotation.peek('generateMigrations')?.boolValue ?? true; @@ -42,18 +43,18 @@ class MigrationGenerator extends GeneratorForAnnotation { } var resolver = await buildStep.resolver; - var ctx = await buildOrmContext(element as ClassElement, annotation, + var ctx = await buildOrmContext({}, element as ClassElement, annotation, buildStep, resolver, autoSnakeCaseNames != false); var lib = generateMigrationLibrary( ctx, element as ClassElement, resolver, buildStep); if (lib == null) return null; - return new DartFormatter().format(lib.accept(new DartEmitter()).toString()); + return DartFormatter().format(lib.accept(DartEmitter()).toString()); } Library generateMigrationLibrary(OrmBuildContext ctx, ClassElement element, Resolver resolver, BuildStep buildStep) { - return new Library((lib) { - lib.body.add(new Class((clazz) { + return Library((lib) { + lib.body.add(Class((clazz) { clazz ..name = '${ctx.buildContext.modelClassName}Migration' ..extend = refer('Migration') @@ -64,7 +65,7 @@ class MigrationGenerator extends GeneratorForAnnotation { } Method buildUpMigration(OrmBuildContext ctx, LibraryBuilder lib) { - return new Method((meth) { + return Method((meth) { var autoIdAndDateFields = const TypeChecker.fromRuntime(Model) .isAssignableFromType(ctx.buildContext.clazz.type); meth @@ -72,20 +73,20 @@ class MigrationGenerator extends GeneratorForAnnotation { ..annotations.add(refer('override')) ..requiredParameters.add(_schemaParam); - //var closure = new Method.closure()..addPositional(parameter('table')); - var closure = new Method((closure) { + //var closure = Method.closure()..addPositional(parameter('table')); + var closure = Method((closure) { closure - ..requiredParameters.add(new Parameter((b) => b..name = 'table')) - ..body = new Block((closureBody) { + ..requiredParameters.add(Parameter((b) => b..name = 'table')) + ..body = Block((closureBody) { var table = refer('table'); List dup = []; ctx.columns.forEach((name, col) { var key = ctx.buildContext.resolveFieldName(name); - if (dup.contains(key)) + if (dup.contains(key)) { return; - else { + } else { // if (key != 'id' || autoIdAndDateFields == false) { // // Check for relationships that might duplicate // for (var rName in ctx.relations.keys) { @@ -111,15 +112,17 @@ class MigrationGenerator extends GeneratorForAnnotation { List positional = [literal(key)]; Map named = {}; - if (autoIdAndDateFields != false && name == 'id') + if (autoIdAndDateFields != false && name == 'id') { methodName = 'serial'; + } if (methodName == null) { switch (col.type) { case ColumnType.varChar: methodName = 'varChar'; - if (col.length != null) + if (col.length != null) { named['length'] = literal(col.length); + } break; case ColumnType.serial: methodName = 'serial'; @@ -196,13 +199,14 @@ class MigrationGenerator extends GeneratorForAnnotation { // Definitely an analyzer issue. } } else { - defaultExpr = new CodeExpression( - new Code(dartObjectToString(defaultValue)), + defaultExpr = CodeExpression( + Code(dartObjectToString(defaultValue)), ); } - if (defaultExpr != null) + if (defaultExpr != null) { cascade.add(refer('defaultsTo').call([defaultExpr])); + } } if (col.indexType == IndexType.primaryKey || @@ -212,20 +216,20 @@ class MigrationGenerator extends GeneratorForAnnotation { cascade.add(refer('unique').call([])); } - if (col.isNullable != true) + if (col.isNullable != true) { cascade.add(refer('notNull').call([])); + } if (cascade.isNotEmpty) { - var b = new StringBuffer() - ..writeln(field.accept(new DartEmitter())); + var b = StringBuffer()..writeln(field.accept(DartEmitter())); for (var ex in cascade) { b ..write('..') - ..writeln(ex.accept(new DartEmitter())); + ..writeln(ex.accept(DartEmitter())); } - field = new CodeExpression(new Code(b.toString())); + field = CodeExpression(Code(b.toString())); } closureBody.addExpression(field); @@ -259,15 +263,16 @@ class MigrationGenerator extends GeneratorForAnnotation { if (relationship.cascadeOnDelete != false && const [RelationshipType.hasOne, RelationshipType.belongsTo] - .contains(relationship.type)) + .contains(relationship.type)) { ref = ref.property('onDeleteCascade').call([]); + } closureBody.addExpression(ref); } }); }); }); - meth.body = new Block((b) { + meth.body = Block((b) { b.addExpression(_schema.property('create').call([ literal(ctx.tableName), closure.closure, @@ -277,12 +282,12 @@ class MigrationGenerator extends GeneratorForAnnotation { } Method buildDownMigration(OrmBuildContext ctx) { - return new Method((b) { + return Method((b) { b ..name = 'down' ..annotations.add(refer('override')) ..requiredParameters.add(_schemaParam) - ..body = new Block((b) { + ..body = Block((b) { var named = {}; if (ctx.relations.values.any((r) => diff --git a/angel_orm_generator/lib/src/orm_build_context.dart b/angel_orm_generator/lib/src/orm_build_context.dart index f22a7c31..4ceea5a0 100644 --- a/angel_orm_generator/lib/src/orm_build_context.dart +++ b/angel_orm_generator/lib/src/orm_build_context.dart @@ -43,7 +43,7 @@ FieldElement findPrimaryFieldInList( var columnAnnotation = columnTypeChecker.firstAnnotationOf(element); if (columnAnnotation != null) { - var column = reviveColumn(new ConstantReader(columnAnnotation)); + var column = reviveColumn(ConstantReader(columnAnnotation)); // print( // ' * Found column on ${field.name} with indexType = ${column.indexType}'); // print(element.metadata); @@ -58,15 +58,14 @@ FieldElement findPrimaryFieldInList( return specialId; } -final Map _cache = {}; - Future buildOrmContext( + Map cache, ClassElement clazz, ConstantReader annotation, BuildStep buildStep, Resolver resolver, bool autoSnakeCaseNames, - {bool heedExclude: true}) async { + {bool heedExclude = true}) async { // Check for @generatedSerializable // ignore: unused_local_variable DartObject generatedSerializable; @@ -79,8 +78,8 @@ Future buildOrmContext( } var id = clazz.location.components.join('-'); - if (_cache.containsKey(id)) { - return _cache[id]; + if (cache.containsKey(id)) { + return cache[id]; } var buildCtx = await buildContext( clazz, annotation, buildStep, resolver, autoSnakeCaseNames, @@ -88,13 +87,13 @@ Future buildOrmContext( var ormAnnotation = reviveORMAnnotation(annotation); // print( // 'tableName (${annotation.objectValue.type.name}) => ${ormAnnotation.tableName} from ${clazz.name} (${annotation.revive().namedArguments})'); - var ctx = new OrmBuildContext( + var ctx = OrmBuildContext( buildCtx, ormAnnotation, (ormAnnotation.tableName?.isNotEmpty == true) ? ormAnnotation.tableName - : pluralize(new ReCase(clazz.name).snakeCase)); - _cache[id] = ctx; + : pluralize(ReCase(clazz.name).snakeCase)); + cache[id] = ctx; // Read all fields for (var field in buildCtx.fields) { @@ -105,7 +104,7 @@ Future buildOrmContext( // print('${element.name} => $columnAnnotation'); if (columnAnnotation != null) { - column = reviveColumn(new ConstantReader(columnAnnotation)); + column = reviveColumn(ConstantReader(columnAnnotation)); } if (column == null && isSpecialId(ctx, field)) { @@ -117,7 +116,7 @@ Future buildOrmContext( if (column == null) { // Guess what kind of column this is... - column = new Column( + column = Column( type: inferColumnType( buildCtx.resolveSerializedFieldType(field.name), ), @@ -125,7 +124,7 @@ Future buildOrmContext( } if (column != null && column.type == null) { - column = new Column( + column = Column( isNullable: column.isNullable, length: column.length, indexType: column.indexType, @@ -139,7 +138,7 @@ Future buildOrmContext( var ann = relationshipTypeChecker.firstAnnotationOf(el); if (ann != null) { - var cr = new ConstantReader(ann); + var cr = ConstantReader(ann); var rc = ctx.buildContext.modelClassNameRecase; var type = cr.read('type').intValue; var localKey = cr.peek('localKey')?.stringValue; @@ -157,7 +156,7 @@ Future buildOrmContext( isListOfModelType(field.type as InterfaceType)) || isModelClass(field.type); if (!canUse) { - throw new UnsupportedError( + throw UnsupportedError( 'Cannot apply relationship to field "${field.name}" - ${field.type} is not assignable to Model.'); } else { try { @@ -173,8 +172,9 @@ Future buildOrmContext( var modelType = firstModelAncestor(refType) ?? refType; foreign = await buildOrmContext( + cache, modelType.element as ClassElement, - new ConstantReader(const TypeChecker.fromRuntime(Orm) + ConstantReader(const TypeChecker.fromRuntime(Orm) .firstAnnotationOf(modelType.element)), buildStep, resolver, @@ -183,8 +183,9 @@ Future buildOrmContext( // Resolve throughType as well if (through != null && through is InterfaceType) { throughContext = await buildOrmContext( + cache, through.element, - new ConstantReader(const TypeChecker.fromRuntime(Serializable) + ConstantReader(const TypeChecker.fromRuntime(Serializable) .firstAnnotationOf(modelType.element)), buildStep, resolver, @@ -196,20 +197,20 @@ Future buildOrmContext( if (ormAnn != null) { foreignTable = - new ConstantReader(ormAnn).peek('tableName')?.stringValue; + ConstantReader(ormAnn).peek('tableName')?.stringValue; } foreignTable ??= pluralize(foreign.buildContext.modelClassNameRecase.snakeCase); } on StackOverflowError { - throw new UnsupportedError( + throw UnsupportedError( 'There is an infinite cycle between ${clazz.name} and ${field.type.name}. This triggered a stack overflow.'); } } } // Fill in missing keys - var rcc = new ReCase(field.name); + var rcc = ReCase(field.name); String keyName(OrmBuildContext ctx, String missing) { var _keyName = @@ -236,7 +237,25 @@ Future buildOrmContext( localKey ??= '${rcc.snakeCase}_$foreignKey'; } - var relation = new RelationshipReader( + // Figure out the join type. + var joinType = JoinType.left; + var joinTypeRdr = cr.peek('joinType')?.objectValue; + if (joinTypeRdr != null) { + // Unfortunately, the analyzer library provides little to nothing + // in the way of reading enums from source, so here's a hack. + var joinTypeType = (joinTypeRdr.type as InterfaceType); + var enumFields = + joinTypeType.element.fields.where((f) => f.isEnumConstant).toList(); + + for (int i = 0; i < enumFields.length; i++) { + if (enumFields[i].constantValue == joinTypeRdr) { + joinType = JoinType.values[i]; + break; + } + } + } + + var relation = RelationshipReader( type, localKey: localKey, foreignKey: foreignKey, @@ -245,34 +264,38 @@ Future buildOrmContext( through: through, foreign: foreign, throughContext: throughContext, + joinType: joinType, ); // print('Relation on ${buildCtx.originalClassName}.${field.name} => ' // 'foreignKey=$foreignKey, localKey=$localKey'); if (relation.type == RelationshipType.belongsTo) { - var name = new ReCase(relation.localKey).camelCase; + var name = ReCase(relation.localKey).camelCase; ctx.buildContext.aliases[name] = relation.localKey; if (!ctx.effectiveFields.any((f) => f.name == field.name)) { var foreignField = relation.findForeignField(ctx); var foreign = relation.throughContext ?? relation.foreign; var type = foreignField.type; - if (isSpecialId(foreign, foreignField)) + if (isSpecialId(foreign, foreignField)) { type = field.type.element.context.typeProvider.intType; - var rf = new RelationFieldImpl(name, relation, type, field); + } + var rf = RelationFieldImpl(name, relation, type, field); ctx.effectiveFields.add(rf); } } ctx.relations[field.name] = relation; } else { - if (column?.type == null) + if (column?.type == null) { throw 'Cannot infer SQL column type for field "${ctx.buildContext.originalClassName}.${field.name}" with type "${field.type.displayName}".'; + } ctx.columns[field.name] = column; - if (!ctx.effectiveFields.any((f) => f.name == field.name)) + if (!ctx.effectiveFields.any((f) => f.name == field.name)) { ctx.effectiveFields.add(field); + } } } @@ -280,22 +303,30 @@ Future buildOrmContext( } ColumnType inferColumnType(DartType type) { - if (const TypeChecker.fromRuntime(String).isAssignableFromType(type)) + if (const TypeChecker.fromRuntime(String).isAssignableFromType(type)) { return ColumnType.varChar; - if (const TypeChecker.fromRuntime(int).isAssignableFromType(type)) + } + if (const TypeChecker.fromRuntime(int).isAssignableFromType(type)) { return ColumnType.int; - if (const TypeChecker.fromRuntime(double).isAssignableFromType(type)) + } + if (const TypeChecker.fromRuntime(double).isAssignableFromType(type)) { return ColumnType.decimal; - if (const TypeChecker.fromRuntime(num).isAssignableFromType(type)) + } + if (const TypeChecker.fromRuntime(num).isAssignableFromType(type)) { return ColumnType.numeric; - if (const TypeChecker.fromRuntime(bool).isAssignableFromType(type)) + } + if (const TypeChecker.fromRuntime(bool).isAssignableFromType(type)) { return ColumnType.boolean; - if (const TypeChecker.fromRuntime(DateTime).isAssignableFromType(type)) + } + if (const TypeChecker.fromRuntime(DateTime).isAssignableFromType(type)) { return ColumnType.timeStamp; - if (const TypeChecker.fromRuntime(Map).isAssignableFromType(type)) + } + if (const TypeChecker.fromRuntime(Map).isAssignableFromType(type)) { return ColumnType.jsonb; - if (const TypeChecker.fromRuntime(List).isAssignableFromType(type)) + } + if (const TypeChecker.fromRuntime(List).isAssignableFromType(type)) { return ColumnType.jsonb; + } if (type is InterfaceType && type.element.isEnum) return ColumnType.int; return null; } @@ -317,10 +348,10 @@ Column reviveColumn(ConstantReader cr) { } if (columnObj != null) { - columnType = new _ColumnType(columnObj); + columnType = _ColumnType(columnObj); } - return new Column( + return Column( isNullable: cr.peek('isNullable')?.boolValue, length: cr.peek('length')?.intValue, type: columnType, @@ -329,7 +360,7 @@ Column reviveColumn(ConstantReader cr) { } const TypeChecker relationshipTypeChecker = - const TypeChecker.fromRuntime(Relationship); + TypeChecker.fromRuntime(Relationship); class OrmBuildContext { final BuildContext buildContext; diff --git a/angel_orm_generator/lib/src/orm_generator.dart b/angel_orm_generator/lib/src/orm_generator.dart index e0f3e6b7..d2e13637 100644 --- a/angel_orm_generator/lib/src/orm_generator.dart +++ b/angel_orm_generator/lib/src/orm_generator.dart @@ -17,14 +17,14 @@ var floatTypes = [ ]; Builder ormBuilder(BuilderOptions options) { - return new SharedPartBuilder([ - new OrmGenerator( + return SharedPartBuilder([ + OrmGenerator( autoSnakeCaseNames: options.config['auto_snake_case_names'] != false) ], 'angel_orm'); } TypeReference futureOf(String type) { - return new TypeReference((b) => b + return TypeReference((b) => b ..symbol = 'Future' ..types.add(refer(type))); } @@ -39,17 +39,17 @@ class OrmGenerator extends GeneratorForAnnotation { Future generateForAnnotatedElement( Element element, ConstantReader annotation, BuildStep buildStep) async { if (element is ClassElement) { - var ctx = await buildOrmContext(element, annotation, buildStep, + var ctx = await buildOrmContext({}, element, annotation, buildStep, buildStep.resolver, autoSnakeCaseNames); var lib = buildOrmLibrary(buildStep.inputId, ctx); - return lib.accept(new DartEmitter()).toString(); + return lib.accept(DartEmitter()).toString(); } else { throw 'The @Orm() annotation can only be applied to classes.'; } } Library buildOrmLibrary(AssetId inputId, OrmBuildContext ctx) { - return new Library((lib) { + return Library((lib) { // Create `FooQuery` class // Create `FooQueryWhere` class lib.body.add(buildQueryClass(ctx)); @@ -59,12 +59,12 @@ class OrmGenerator extends GeneratorForAnnotation { } Class buildQueryClass(OrmBuildContext ctx) { - return new Class((clazz) { + return Class((clazz) { var rc = ctx.buildContext.modelClassNameRecase; var queryWhereType = refer('${rc.pascalCase}QueryWhere'); clazz ..name = '${rc.pascalCase}Query' - ..extend = new TypeReference((b) { + ..extend = TypeReference((b) { b ..symbol = 'Query' ..types.addAll([ @@ -96,7 +96,7 @@ class OrmGenerator extends GeneratorForAnnotation { })); // Add values - clazz.fields.add(new Field((b) { + clazz.fields.add(Field((b) { var type = refer('${rc.pascalCase}QueryValues'); b ..name = 'values' @@ -107,23 +107,23 @@ class OrmGenerator extends GeneratorForAnnotation { })); // Add tableName - clazz.methods.add(new Method((m) { + clazz.methods.add(Method((m) { m ..name = 'tableName' ..annotations.add(refer('override')) ..type = MethodType.getter - ..body = new Block((b) { + ..body = Block((b) { b.addExpression(literalString(ctx.tableName).returned); }); })); // Add fields getter - clazz.methods.add(new Method((m) { + clazz.methods.add(Method((m) { m ..name = 'fields' ..annotations.add(refer('override')) ..type = MethodType.getter - ..body = new Block((b) { + ..body = Block((b) { var names = ctx.effectiveFields .map((f) => literalString(ctx.buildContext.resolveFieldName(f.name))) @@ -133,41 +133,41 @@ class OrmGenerator extends GeneratorForAnnotation { })); // Add _where member - clazz.fields.add(new Field((b) { + clazz.fields.add(Field((b) { b ..name = '_where' ..type = queryWhereType; })); // Add where getter - clazz.methods.add(new Method((b) { + clazz.methods.add(Method((b) { b ..name = 'where' ..type = MethodType.getter ..returns = queryWhereType ..annotations.add(refer('override')) - ..body = new Block((b) => b.addExpression(refer('_where').returned)); + ..body = Block((b) => b.addExpression(refer('_where').returned)); })); // newWhereClause() - clazz.methods.add(new Method((b) { + clazz.methods.add(Method((b) { b ..name = 'newWhereClause' ..annotations.add(refer('override')) ..returns = queryWhereType - ..body = new Block((b) => b.addExpression( + ..body = Block((b) => b.addExpression( queryWhereType.newInstance([refer('this')]).returned)); })); // Add deserialize() - clazz.methods.add(new Method((m) { + clazz.methods.add(Method((m) { m ..name = 'parseRow' ..static = true ..returns = ctx.buildContext.modelClassType - ..requiredParameters.add(new Parameter((b) => b + ..requiredParameters.add(Parameter((b) => b ..name = 'row' ..type = refer('List'))) - ..body = new Block((b) { + ..body = Block((b) { int i = 0; var args = {}; @@ -177,11 +177,11 @@ class OrmGenerator extends GeneratorForAnnotation { if (isSpecialId(ctx, field)) type = refer('int'); var expr = (refer('row').index(literalNum(i++))); - if (isSpecialId(ctx, field)) + if (isSpecialId(ctx, field)) { expr = expr.property('toString').call([]); - else if (field is RelationFieldImpl) + } else if (field is RelationFieldImpl) { continue; - else if (ctx.columns[field.name]?.type == ColumnType.json) { + } else if (ctx.columns[field.name]?.type == ColumnType.json) { expr = refer('json') .property('decode') .call([expr.asA(refer('String'))]).asA(type); @@ -193,14 +193,15 @@ class OrmGenerator extends GeneratorForAnnotation { var isNull = expr.equalTo(literalNull); expr = isNull.conditional(literalNull, type.property('values').index(expr.asA(refer('int')))); - } else + } else { expr = expr.asA(type); + } args[field.name] = expr; } b.statements - .add(new Code('if (row.every((x) => x == null)) return null;')); + .add(Code('if (row.every((x) => x == null)) return null;')); b.addExpression(ctx.buildContext.modelClassType .newInstance([], args).assignVar('model')); @@ -230,11 +231,11 @@ class OrmGenerator extends GeneratorForAnnotation { } var expr = refer('model').property('copyWith').call([], {name: parsed}); - var block = new Block( - (b) => b.addExpression(refer('model').assign(expr))); - var blockStr = block.accept(new DartEmitter()); + var block = + Block((b) => b.addExpression(refer('model').assign(expr))); + var blockStr = block.accept(DartEmitter()); var ifStr = 'if (row.length > $i) { $blockStr }'; - b.statements.add(new Code(ifStr)); + b.statements.add(Code(ifStr)); i += relation.foreign.effectiveFields.length; }); @@ -242,28 +243,33 @@ class OrmGenerator extends GeneratorForAnnotation { }); })); - clazz.methods.add(new Method((m) { + clazz.methods.add(Method((m) { m ..name = 'deserialize' ..annotations.add(refer('override')) - ..requiredParameters.add(new Parameter((b) => b + ..requiredParameters.add(Parameter((b) => b ..name = 'row' ..type = refer('List'))) - ..body = new Block((b) { + ..body = Block((b) { b.addExpression(refer('parseRow').call([refer('row')]).returned); }); })); // If there are any relations, we need some overrides. - clazz.constructors.add(new Constructor((b) { + clazz.constructors.add(Constructor((b) { b + ..optionalParameters.add(Parameter((b) => b + ..named = true + ..name = 'parent' + ..type = refer('Query'))) ..optionalParameters.add(Parameter((b) => b ..named = true ..name = 'trampoline' ..type = TypeReference((b) => b ..symbol = 'Set' ..types.add(refer('String'))))) - ..body = new Block((b) { + ..initializers.add(Code('super(parent: parent)')) + ..body = Block((b) { b.statements.addAll([ Code('trampoline ??= Set();'), Code('trampoline.add(tableName);'), @@ -275,6 +281,7 @@ class OrmGenerator extends GeneratorForAnnotation { .assign(queryWhereType.newInstance([refer('this')])), ); + // Note: this is where subquery fields for relations are added. ctx.relations.forEach((fieldName, relation) { //var name = ctx.buildContext.resolveFieldName(fieldName); if (relation.type == RelationshipType.belongsTo || @@ -283,29 +290,106 @@ class OrmGenerator extends GeneratorForAnnotation { var foreign = relation.throughContext ?? relation.foreign; // If this is a many-to-many, add the fields from the other object. - var additionalFields = relation.foreign.effectiveFields - // .where((f) => f.name != 'id' || !isSpecialId(ctx, f)) - .map((f) => literalString(relation.foreign.buildContext - .resolveFieldName(f.name))); + + var additionalStrs = relation.foreign.effectiveFields.map((f) => + relation.foreign.buildContext.resolveFieldName(f.name)); + var additionalFields = additionalStrs.map(literalString); var joinArgs = [relation.localKey, relation.foreignKey] .map(literalString) .toList(); - // Instead of passing the table as-is, we'll compile a subquery. - if (relation.type == RelationshipType.hasMany) { - var foreignQueryType = - foreign.buildContext.modelClassNameRecase.pascalCase + - 'Query'; - joinArgs.insert( - 0, - refer(foreignQueryType).newInstance( - [], {'trampoline': refer('trampoline')})); + // In the case of a many-to-many, we don't generate a subquery field, + // as it easily leads to stack overflows. + if (relation.isManyToMany) { + // We can't simply join against the "through" table; this itself must + // be a join. + // (SELECT role_users.role_id, + // FROM users + // LEFT JOIN role_users ON role_users.user_id=users.id) + var foreignFields = additionalStrs + .map((f) => '${relation.foreign.tableName}.$f'); + var b = StringBuffer('(SELECT '); + // role_users.role_id + b.write('${relation.throughContext.tableName}'); + b.write('.${relation.foreignKey}'); + // , + b.write(foreignFields.isEmpty + ? '' + : ', ' + foreignFields.join(', ')); + // FROM users + b.write(' FROM '); + b.write(relation.foreign.tableName); + // LEFT JOIN role_users + b.write(' LEFT JOIN ${relation.throughContext.tableName}'); + // Figure out which field on the "through" table points to users (foreign). + var throughRelation = + relation.throughContext.relations.values.firstWhere((e) { + return e.foreignTable == relation.foreign.tableName; + }, orElse: () { + // _Role has a many-to-many to _User through _RoleUser, but + // _RoleUser has no relation pointing to _User. + var b = StringBuffer(); + b.write(ctx.buildContext.modelClassName); + b.write('has a many-to-many relationship to '); + b.write(relation.foreign.buildContext.modelClassName); + b.write(' through '); + b.write( + relation.throughContext.buildContext.modelClassName); + b.write(', but '); + b.write( + relation.throughContext.buildContext.modelClassName); + b.write('has no relation pointing to '); + b.write(relation.foreign.buildContext.modelClassName); + b.write('.'); + throw b.toString(); + }); + + // ON role_users.user_id=users.id) + b.write(' ON '); + b.write('${relation.throughContext.tableName}'); + b.write('.'); + b.write(throughRelation.localKey); + b.write('='); + b.write(relation.foreign.tableName); + b.write('.'); + b.write(throughRelation.foreignKey); + b.write(')'); + + joinArgs.insert(0, literalString(b.toString())); } else { - joinArgs.insert(0, literalString(foreign.tableName)); + // In the past, we would either do a join on the table name + // itself, or create an instance of a query. + // + // From this point on, however, we will create a field for each + // join, so that users can customize the generated query. + // + // There'll be a private `_field`, and then a getter, named `field`, + // that returns the subquery object. + var foreignQueryType = refer( + foreign.buildContext.modelClassNameRecase.pascalCase + + 'Query'); + clazz + ..fields.add(Field((b) => b + ..name = '_$fieldName' + ..type = foreignQueryType)) + ..methods.add(Method((b) => b + ..name = fieldName + ..type = MethodType.getter + ..returns = foreignQueryType + ..body = refer('_$fieldName').returned.statement)); + + // Assign a value to `_field`. + var queryInstantiation = foreignQueryType.newInstance([], { + 'trampoline': refer('trampoline'), + 'parent': refer('this') + }); + joinArgs.insert( + 0, refer('_$fieldName').assign(queryInstantiation)); } - b.addExpression(refer('leftJoin').call(joinArgs, { + var joinType = relation.joinTypeString; + b.addExpression(refer(joinType).call(joinArgs, { 'additionalFields': literalConstList(additionalFields.toList()), 'trampoline': refer('trampoline'), @@ -331,12 +415,11 @@ class OrmGenerator extends GeneratorForAnnotation { }); var out = outExprs.reduce((a, b) => a.and(b)); - clazz.methods.add(new Method((b) { + clazz.methods.add(Method((b) { b ..name = 'canCompile' ..annotations.add(refer('override')) - ..requiredParameters - .add(new Parameter((b) => b..name = 'trampoline')) + ..requiredParameters.add(Parameter((b) => b..name = 'trampoline')) ..returns = refer('bool') ..body = Block((b) { b.addExpression(out.returned); @@ -344,134 +427,16 @@ class OrmGenerator extends GeneratorForAnnotation { })); } - // TODO: Ultimately remove the insert override - if (false && ctx.relations.isNotEmpty) { - clazz.methods.add(new Method((b) { - b - ..name = 'insert' - ..annotations.add(refer('override')) - ..requiredParameters.add(new Parameter((b) => b..name = 'executor')) - ..body = new Block((b) { - var inTransaction = new Method((b) { - b - ..modifier = MethodModifier.async - ..body = new Block((b) { - b.addExpression(refer('super') - .property('insert') - .call([refer('executor')]) - .awaited - .assignVar('result')); - - // Just call getOne() again - if (ctx.effectiveFields.any((f) => - isSpecialId(ctx, f) || - (ctx.columns[f.name]?.indexType == - IndexType.primaryKey))) { - b.addExpression(refer('where') - .property('id') - .property('equals') - .call([ - (refer('int') - .property('tryParse') - .call([refer('result').property('id')])) - ])); - - b.addExpression(refer('result').assign( - refer('getOne').call([refer('executor')]).awaited)); - } - - // TODO: Remove - Fetch the results of @hasMany - // ctx.relations.forEach((name, relation) { - // if (relation.type == RelationshipType.hasMany) { - // // Call fetchLinked(); - // var fetchLinked = refer('fetchLinked') - // .call([refer('result'), refer('executor')]).awaited; - // b.addExpression(refer('result').assign(fetchLinked)); - // } - // }); - - b.addExpression(refer('result').returned); - }); - }); - - b.addExpression(refer('executor') - .property('transaction') - .call([inTransaction.closure]).returned); - }); - })); - } - - // Create a Future fetchLinked(T model, QueryExecutor), if necessary. - if (false && - ctx.relations.values.any((r) => r.type == RelationshipType.hasMany)) { - clazz.methods.add(new Method((b) { - b - ..name = 'fetchLinked' - ..modifier = MethodModifier.async - ..returns = new TypeReference((b) { - b - ..symbol = 'Future' - ..types.add(ctx.buildContext.modelClassType); - }) - ..requiredParameters.addAll([ - new Parameter((b) => b - ..name = 'model' - ..type = ctx.buildContext.modelClassType), - new Parameter((b) => b - ..name = 'executor' - ..type = refer('QueryExecutor')), - ]) - ..body = new Block((b) { - var args = {}; - - ctx.relations.forEach((name, relation) { - // TODO: Should this be entirely removed? - if (relation.type == RelationshipType.hasMany) { - // For each hasMany, we need to create a query of - // the corresponding type. - var foreign = relation.foreign; - var queryType = refer( - '${foreign.buildContext.modelClassNameRecase.pascalCase}Query'); - var queryInstance = queryType.newInstance([]); - - // Next, we need to apply a cascade that sets the correct query value. - var localField = relation.findLocalField(ctx); - var foreignField = relation.findForeignField(ctx); - - var queryValue = (isSpecialId(ctx, localField)) - ? 'int.parse(model.id)' - : 'model.${localField.name}'; - var cascadeText = - '..where.${foreignField.name}.equals($queryValue)'; - var queryText = queryInstance.accept(new DartEmitter()); - var combinedExpr = - new CodeExpression(new Code('($queryText$cascadeText)')); - - // Finally, just call get and await it. - var expr = combinedExpr - .property('get') - .call([refer('executor')]).awaited; - args[name] = expr; - } - }); - - // Just return a copyWith - b.addExpression( - refer('model').property('copyWith').call([], args).returned); - }); - })); - } - // Also, if there is a @HasMany, generate overrides for query methods that // execute in a transaction, and invoke fetchLinked. if (ctx.relations.values.any((r) => r.type == RelationshipType.hasMany)) { for (var methodName in const ['get', 'update', 'delete']) { - clazz.methods.add(new Method((b) { + clazz.methods.add(Method((b) { var type = ctx.buildContext.modelClassType.accept(DartEmitter()); b ..name = methodName ..annotations.add(refer('override')) - ..requiredParameters.add(new Parameter((b) => b + ..requiredParameters.add(Parameter((b) => b ..name = 'executor' ..type = refer('QueryExecutor'))); @@ -500,7 +465,7 @@ class OrmGenerator extends GeneratorForAnnotation { '@HasMany and @ManyToMany relations require a primary key to be defined on the model.'; } - b.body = new Code(''' + b.body = Code(''' return super.$methodName(executor).then((result) { return result.fold>([], (out, model) { var idx = out.indexWhere((m) => m.$keyName == model.$keyName); @@ -521,19 +486,19 @@ class OrmGenerator extends GeneratorForAnnotation { } Class buildWhereClass(OrmBuildContext ctx) { - return new Class((clazz) { + return Class((clazz) { var rc = ctx.buildContext.modelClassNameRecase; clazz ..name = '${rc.pascalCase}QueryWhere' ..extend = refer('QueryWhere'); // Build expressionBuilders getter - clazz.methods.add(new Method((m) { + clazz.methods.add(Method((m) { m ..name = 'expressionBuilders' ..annotations.add(refer('override')) ..type = MethodType.getter - ..body = new Block((b) { + ..body = Block((b) { var references = ctx.effectiveFields.map((f) => refer(f.name)); b.addExpression(literalList(references).returned); }); @@ -557,11 +522,11 @@ class OrmGenerator extends GeneratorForAnnotation { if (const TypeChecker.fromRuntime(int).isExactlyType(type) || const TypeChecker.fromRuntime(double).isExactlyType(type) || isSpecialId(ctx, field)) { - builderType = new TypeReference((b) => b + builderType = TypeReference((b) => b ..symbol = 'NumericSqlExpressionBuilder' ..types.add(refer(isSpecialId(ctx, field) ? 'int' : type.name))); } else if (type is InterfaceType && type.element.isEnum) { - builderType = new TypeReference((b) => b + builderType = TypeReference((b) => b ..symbol = 'EnumSqlExpressionBuilder' ..types.add(convertTypeReference(type))); args.add(CodeExpression(Code('(v) => v.index'))); @@ -580,20 +545,20 @@ class OrmGenerator extends GeneratorForAnnotation { builderType = refer('ListSqlExpressionBuilder'); } else if (ctx.relations.containsKey(field.name)) { var relation = ctx.relations[field.name]; - if (relation.type != RelationshipType.belongsTo) + if (relation.type != RelationshipType.belongsTo) { continue; - else { - builderType = new TypeReference((b) => b + } else { + builderType = TypeReference((b) => b ..symbol = 'NumericSqlExpressionBuilder' ..types.add(refer('int'))); name = relation.localKey; } } else { - throw new UnsupportedError( + throw UnsupportedError( 'Cannot generate ORM code for field of type ${field.type.name}.'); } - clazz.fields.add(new Field((b) { + clazz.fields.add(Field((b) { b ..name = name ..modifier = FieldModifier.final$ @@ -611,9 +576,9 @@ class OrmGenerator extends GeneratorForAnnotation { } // Now, just add a constructor that initializes each builder. - clazz.constructors.add(new Constructor((b) { + clazz.constructors.add(Constructor((b) { b - ..requiredParameters.add(new Parameter((b) => b + ..requiredParameters.add(Parameter((b) => b ..name = 'query' ..type = refer('${rc.pascalCase}Query'))) ..initializers.addAll(initializers); @@ -622,7 +587,7 @@ class OrmGenerator extends GeneratorForAnnotation { } Class buildValuesClass(OrmBuildContext ctx) { - return new Class((clazz) { + return Class((clazz) { var rc = ctx.buildContext.modelClassNameRecase; clazz ..name = '${rc.pascalCase}QueryValues' @@ -660,7 +625,7 @@ class OrmGenerator extends GeneratorForAnnotation { var name = ctx.buildContext.resolveFieldName(field.name); var type = convertTypeReference(field.type); - clazz.methods.add(new Method((b) { + clazz.methods.add(Method((b) { var value = refer('values').index(literalString(name)); if (fType is InterfaceType && fType.element.isEnum) { @@ -684,10 +649,10 @@ class OrmGenerator extends GeneratorForAnnotation { ..name = field.name ..type = MethodType.getter ..returns = type - ..body = new Block((b) => b.addExpression(value.returned)); + ..body = Block((b) => b.addExpression(value.returned)); })); - clazz.methods.add(new Method((b) { + clazz.methods.add(Method((b) { Expression value = refer('value'); if (fType is InterfaceType && fType.element.isEnum) { @@ -702,7 +667,7 @@ class OrmGenerator extends GeneratorForAnnotation { b ..name = field.name ..type = MethodType.setter - ..requiredParameters.add(new Parameter((b) => b + ..requiredParameters.add(Parameter((b) => b ..name = 'value' ..type = type)) ..body = @@ -711,17 +676,18 @@ class OrmGenerator extends GeneratorForAnnotation { } // Add an copyFrom(model) - clazz.methods.add(new Method((b) { + clazz.methods.add(Method((b) { b ..name = 'copyFrom' ..returns = refer('void') - ..requiredParameters.add(new Parameter((b) => b + ..requiredParameters.add(Parameter((b) => b ..name = 'model' ..type = ctx.buildContext.modelClassType)) - ..body = new Block((b) { + ..body = Block((b) { for (var field in ctx.effectiveFields) { - if (isSpecialId(ctx, field) || field is RelationFieldImpl) + if (isSpecialId(ctx, field) || field is RelationFieldImpl) { continue; + } b.addExpression(refer(field.name) .assign(refer('model').property(field.name))); } @@ -744,11 +710,11 @@ class OrmGenerator extends GeneratorForAnnotation { } var cond = prop.notEqualTo(literalNull); - var condStr = cond.accept(new DartEmitter()); + var condStr = cond.accept(DartEmitter()); var blkStr = - new Block((b) => b.addExpression(target.assign(parsedId))) - .accept(new DartEmitter()); - var ifStmt = new Code('if ($condStr) { $blkStr }'); + Block((b) => b.addExpression(target.assign(parsedId))) + .accept(DartEmitter()); + var ifStmt = Code('if ($condStr) { $blkStr }'); b.statements.add(ifStmt); } } diff --git a/angel_orm_generator/lib/src/readers.dart b/angel_orm_generator/lib/src/readers.dart index 4017168b..bef5f0f2 100644 --- a/angel_orm_generator/lib/src/readers.dart +++ b/angel_orm_generator/lib/src/readers.dart @@ -5,7 +5,7 @@ import 'package:angel_orm/angel_orm.dart'; import 'package:source_gen/source_gen.dart'; import 'orm_build_context.dart'; -const TypeChecker columnTypeChecker = const TypeChecker.fromRuntime(Column); +const TypeChecker columnTypeChecker = TypeChecker.fromRuntime(Column); Orm reviveORMAnnotation(ConstantReader reader) { return Orm( @@ -34,6 +34,7 @@ class RelationshipReader { final DartType through; final OrmBuildContext foreign; final OrmBuildContext throughContext; + final JoinType joinType; const RelationshipReader(this.type, {this.localKey, @@ -42,11 +43,29 @@ class RelationshipReader { this.cascadeOnDelete, this.through, this.foreign, - this.throughContext}); + this.throughContext, + this.joinType}); bool get isManyToMany => type == RelationshipType.hasMany && throughContext != null; + String get joinTypeString { + switch (joinType ?? JoinType.left) { + case JoinType.inner: + return 'join'; + case JoinType.left: + return 'leftJoin'; + case JoinType.right: + return 'rightJoin'; + case JoinType.full: + return 'fullOuterJoin'; + case JoinType.self: + return 'selfJoin'; + default: + return 'join'; + } + } + FieldElement findLocalField(OrmBuildContext ctx) { return ctx.effectiveFields.firstWhere( (f) => ctx.buildContext.resolveFieldName(f.name) == localKey, diff --git a/angel_orm_generator/pubspec.yaml b/angel_orm_generator/pubspec.yaml index 7c66ed04..75f33075 100644 --- a/angel_orm_generator/pubspec.yaml +++ b/angel_orm_generator/pubspec.yaml @@ -1,18 +1,18 @@ name: angel_orm_generator -version: 2.0.5 +version: 2.1.0-beta.1 description: Code generators for Angel's ORM. Generates query builder classes. author: Tobe O homepage: https://github.com/angel-dart/orm environment: - sdk: ">=2.0.0-dev <3.0.0" + sdk: ">=2.0.0<3.0.0" dependencies: - analyzer: ">=0.27.1 <2.0.0" + analyzer: ">=0.35.0 <2.0.0" angel_model: ^1.0.0 angel_serialize: ^2.0.0 - angel_orm: ^2.0.0-dev + angel_orm: ^2.1.0-beta angel_serialize_generator: ^2.0.0 - build: ">=0.12.0 <2.0.0" - build_config: ">=0.3.0 <0.5.0" + build: ^1.0.0 + build_config: ^0.4.0 code_builder: ^3.0.0 dart_style: ^1.0.0 inflection2: ^0.4.2 @@ -23,12 +23,11 @@ dependencies: dev_dependencies: angel_framework: ^2.0.0-alpha angel_migration: - git: - url: https://github.com/angel-dart/migration - path: angel_migration + path: ../angel_migration #angel_test: ^1.0.0 build_runner: ^1.0.0 collection: ^1.0.0 + pedantic: ^1.0.0 postgres: ^1.0.0 test: ^1.0.0 # dependency_overrides: diff --git a/angel_orm_mysql/example/main.g.dart b/angel_orm_mysql/example/main.g.dart index f35cf8fe..56bcf3e1 100644 --- a/angel_orm_mysql/example/main.g.dart +++ b/angel_orm_mysql/example/main.g.dart @@ -153,7 +153,7 @@ class TodoQueryValues extends MapQueryValues { class Todo extends _Todo { Todo( {this.id, - @required this.isComplete = false, + this.isComplete = false, this.text, this.createdAt, this.updatedAt}); diff --git a/angel_orm_mysql/lib/angel_orm_mysql.dart b/angel_orm_mysql/lib/angel_orm_mysql.dart index 50ac6521..fdda00d8 100644 --- a/angel_orm_mysql/lib/angel_orm_mysql.dart +++ b/angel_orm_mysql/lib/angel_orm_mysql.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:angel_orm/angel_orm.dart'; import 'package:angel_orm/src/query.dart'; import 'package:logging/logging.dart'; -import 'package:pool/pool.dart'; +// import 'package:pool/pool.dart'; import 'package:sqljocky5/connection/connection.dart'; import 'package:sqljocky5/sqljocky.dart'; diff --git a/angel_orm_mysql/pubspec.yaml b/angel_orm_mysql/pubspec.yaml index d7b92dcf..2beabb75 100644 --- a/angel_orm_mysql/pubspec.yaml +++ b/angel_orm_mysql/pubspec.yaml @@ -17,4 +17,7 @@ dev_dependencies: angel_orm_test: path: ../angel_orm_test build_runner: ^1.0.0 - test: ^1.0.0 \ No newline at end of file + test: ^1.0.0 +dependency_overrides: + angel_migration: + path: ../angel_migration \ No newline at end of file diff --git a/angel_orm_postgres/CHANGELOG.md b/angel_orm_postgres/CHANGELOG.md index 160eb6e3..0d8c522a 100644 --- a/angel_orm_postgres/CHANGELOG.md +++ b/angel_orm_postgres/CHANGELOG.md @@ -1,3 +1,6 @@ +# 1.1.0-beta +* Updates for `package:angel_orm@2.1.0-beta`. + # 1.0.0 * Bump to `1.0.0`. This package has actually been stable for several months. diff --git a/angel_orm_postgres/lib/angel_orm_postgres.dart b/angel_orm_postgres/lib/angel_orm_postgres.dart index 182370de..fb522bad 100644 --- a/angel_orm_postgres/lib/angel_orm_postgres.dart +++ b/angel_orm_postgres/lib/angel_orm_postgres.dart @@ -24,7 +24,13 @@ class PostgreSqlExecutor extends QueryExecutor { PostgreSQLExecutionContext get connection => _connection; /// Closes the connection. - Future close() => (_connection as PostgreSQLConnection).close(); + Future close() { + if (_connection is PostgreSQLConnection) { + return (_connection as PostgreSQLConnection).close(); + } else { + return Future.value(); + } + } @override Future> query( @@ -42,18 +48,16 @@ class PostgreSqlExecutor extends QueryExecutor { } @override - Future transaction(FutureOr Function() f) async { - if (_connection is! PostgreSQLConnection) return await f(); - var old = _connection; + Future transaction(FutureOr Function(QueryExecutor) f) async { + if (_connection is! PostgreSQLConnection) return await f(this); T result; try { logger?.fine('Entering transaction'); await (_connection as PostgreSQLConnection).transaction((ctx) async { - _connection = ctx; - result = await f(); + var tx = PostgreSqlExecutor(ctx, logger: logger); + result = await f(tx); }); } finally { - _connection = old; logger?.fine('Exiting transaction'); return result; } @@ -130,7 +134,7 @@ class PostgreSqlExecutorPool extends QueryExecutor { } @override - Future transaction(FutureOr Function() f) { + Future transaction(FutureOr Function(QueryExecutor) f) { return _pool.withResource(() async { var executor = await _next(); return executor.transaction(f); diff --git a/angel_orm_postgres/pubspec.yaml b/angel_orm_postgres/pubspec.yaml index 5fabea04..2015c692 100644 --- a/angel_orm_postgres/pubspec.yaml +++ b/angel_orm_postgres/pubspec.yaml @@ -1,16 +1,20 @@ name: angel_orm_postgres -version: 1.0.0 +version: 1.1.0-beta description: PostgreSQL support for Angel's ORM. Includes functionality for querying and transactions. author: Tobe O homepage: https://github.com/angel-dart/orm environment: sdk: '>=2.0.0-dev.1.2 <3.0.0' dependencies: - angel_orm: ^2.0.0-dev + angel_orm: ^2.1.0-beta logging: ^0.11.0 pool: ^1.0.0 postgres: ^1.0.0 dev_dependencies: angel_orm_test: path: ../angel_orm_test - test: ^1.0.0 \ No newline at end of file + pretty_logging: ^1.0.0 + test: ^1.0.0 +# dependency_overrides: +# angel_orm: +# path: ../angel_orm \ No newline at end of file diff --git a/angel_orm_postgres/test/all_test.dart b/angel_orm_postgres/test/all_test.dart index aaa3c4ef..467cd0f5 100644 --- a/angel_orm_postgres/test/all_test.dart +++ b/angel_orm_postgres/test/all_test.dart @@ -1,14 +1,13 @@ import 'package:angel_orm_test/angel_orm_test.dart'; import 'package:logging/logging.dart'; +import 'package:pretty_logging/pretty_logging.dart'; import 'package:test/test.dart'; import 'common.dart'; void main() { - Logger.root.onRecord.listen((rec) { - print(rec); - if (rec.error != null) print(rec.error); - if (rec.stackTrace != null) print(rec.stackTrace); - }); + Logger.root + ..level = Level.ALL + ..onRecord.listen(prettyLog); group('postgresql', () { group('belongsTo', diff --git a/angel_orm_service/example/todo.g.dart b/angel_orm_service/example/todo.g.dart index a99cfd4d..6ac25846 100644 --- a/angel_orm_service/example/todo.g.dart +++ b/angel_orm_service/example/todo.g.dart @@ -179,7 +179,7 @@ class Todo extends _Todo { String text, DateTime createdAt, DateTime updatedAt}) { - return new Todo( + return Todo( id: id ?? this.id, isComplete: isComplete ?? this.isComplete, text: text ?? this.text, @@ -215,7 +215,7 @@ class Todo extends _Todo { // SerializerGenerator // ************************************************************************** -const TodoSerializer todoSerializer = const TodoSerializer(); +const TodoSerializer todoSerializer = TodoSerializer(); class TodoEncoder extends Converter { const TodoEncoder(); @@ -240,10 +240,10 @@ class TodoSerializer extends Codec { get decoder => const TodoDecoder(); static Todo fromMap(Map map) { if (map['text'] == null) { - throw new FormatException("Missing required field 'text' on Todo."); + throw FormatException("Missing required field 'text' on Todo."); } - return new Todo( + return Todo( id: map['id'] as String, isComplete: map['is_complete'] as bool ?? false, text: map['text'] as String, @@ -264,7 +264,7 @@ class TodoSerializer extends Codec { return null; } if (model.text == null) { - throw new FormatException("Missing required field 'text' on Todo."); + throw FormatException("Missing required field 'text' on Todo."); } return { diff --git a/angel_orm_service/lib/angel_orm_service.dart b/angel_orm_service/lib/angel_orm_service.dart index 4e6ab6b1..e3b6c57d 100644 --- a/angel_orm_service/lib/angel_orm_service.dart +++ b/angel_orm_service/lib/angel_orm_service.dart @@ -67,9 +67,9 @@ class OrmService> if (v is Map) { v.forEach((key, value) { var descending = false; - if (value is String) + if (value is String) { descending = value == '-1'; - else if (value is num) descending = value.toInt() == -1; + } else if (value is num) descending = value.toInt() == -1; query.orderBy(key.toString(), descending: descending); }); } else if (v is String) { @@ -120,8 +120,7 @@ class OrmService> await _applyQuery(query, params); var result = await query.getOne(executor); if (result != null) return result; - throw new AngelHttpException.notFound( - message: 'No record found for ID $id'); + throw AngelHttpException.notFound(message: 'No record found for ID $id'); } @override @@ -133,7 +132,7 @@ class OrmService> await _applyQuery(query, params); var result = await query.getOne(executor); if (result != null) return result; - throw new AngelHttpException.notFound(message: errorMessage); + throw AngelHttpException.notFound(message: errorMessage); } @override @@ -170,8 +169,7 @@ class OrmService> var result = await query.updateOne(executor); if (result != null) return result; - throw new AngelHttpException.notFound( - message: 'No record found for ID $id'); + throw AngelHttpException.notFound(message: 'No record found for ID $id'); } @override @@ -192,7 +190,6 @@ class OrmService> var result = await query.deleteOne(executor); if (result != null) return result; - throw new AngelHttpException.notFound( - message: 'No record found for ID $id'); + throw AngelHttpException.notFound(message: 'No record found for ID $id'); } } diff --git a/angel_orm_service/pubspec.yaml b/angel_orm_service/pubspec.yaml index 40607677..9ef79e75 100644 --- a/angel_orm_service/pubspec.yaml +++ b/angel_orm_service/pubspec.yaml @@ -23,4 +23,7 @@ dev_dependencies: logging: ^0.11.0 pedantic: ^1.0.0 postgres: ^1.0.0 - test: ^1.0.0 \ No newline at end of file + test: ^1.0.0 +dependency_overrides: + angel_migration: + path: ../angel_migration \ No newline at end of file diff --git a/angel_orm_service/test/pokemon.g.dart b/angel_orm_service/test/pokemon.g.dart index d6e49ed9..aa769e33 100644 --- a/angel_orm_service/test/pokemon.g.dart +++ b/angel_orm_service/test/pokemon.g.dart @@ -238,7 +238,7 @@ class Pokemon extends _Pokemon { PokemonType type2, DateTime createdAt, DateTime updatedAt}) { - return new Pokemon( + return Pokemon( id: id ?? this.id, species: species ?? this.species, name: name ?? this.name, @@ -281,7 +281,7 @@ class Pokemon extends _Pokemon { // SerializerGenerator // ************************************************************************** -const PokemonSerializer pokemonSerializer = const PokemonSerializer(); +const PokemonSerializer pokemonSerializer = PokemonSerializer(); class PokemonEncoder extends Converter { const PokemonEncoder(); @@ -306,18 +306,18 @@ class PokemonSerializer extends Codec { get decoder => const PokemonDecoder(); static Pokemon fromMap(Map map) { if (map['species'] == null) { - throw new FormatException("Missing required field 'species' on Pokemon."); + throw FormatException("Missing required field 'species' on Pokemon."); } if (map['level'] == null) { - throw new FormatException("Missing required field 'level' on Pokemon."); + throw FormatException("Missing required field 'level' on Pokemon."); } if (map['type1'] == null) { - throw new FormatException("Missing required field 'type1' on Pokemon."); + throw FormatException("Missing required field 'type1' on Pokemon."); } - return new Pokemon( + return Pokemon( id: map['id'] as String, species: map['species'] as String, name: map['name'] as String, @@ -349,15 +349,15 @@ class PokemonSerializer extends Codec { return null; } if (model.species == null) { - throw new FormatException("Missing required field 'species' on Pokemon."); + throw FormatException("Missing required field 'species' on Pokemon."); } if (model.level == null) { - throw new FormatException("Missing required field 'level' on Pokemon."); + throw FormatException("Missing required field 'level' on Pokemon."); } if (model.type1 == null) { - throw new FormatException("Missing required field 'type1' on Pokemon."); + throw FormatException("Missing required field 'type1' on Pokemon."); } return { diff --git a/angel_orm_test/lib/src/belongs_to_test.dart b/angel_orm_test/lib/src/belongs_to_test.dart index cef8344a..c58dc7f8 100644 --- a/angel_orm_test/lib/src/belongs_to_test.dart +++ b/angel_orm_test/lib/src/belongs_to_test.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:angel_orm/angel_orm.dart'; import 'package:test/test.dart'; import 'models/book.dart'; +import 'util.dart'; belongsToTests(FutureOr Function() createExecutor, {FutureOr Function(QueryExecutor) close}) { @@ -124,8 +125,9 @@ belongsToTests(FutureOr Function() createExecutor, }); test('delete stream', () async { + printSeparator('Delete stream test'); var query = new BookQuery()..where.name.equals(deathlyHallows.name); - print(query.compile(Set())); + print(query.compile(Set(), preamble: 'DELETE', withFields: false)); var books = await query.delete(executor); expect(books, hasLength(1)); @@ -146,4 +148,21 @@ belongsToTests(FutureOr Function() createExecutor, expect(book.author, isNotNull); expect(book.author.name, jkRowling.name); }); + + group('joined subquery', () { + // To verify that the joined subquery is correct, + // we test both a query that return empty, and one + // that should return correctly. + test('returns empty on false subquery', () async { + printSeparator('False subquery test'); + var query = BookQuery()..author.where.name.equals('Billie Jean'); + expect(await query.get(executor), isEmpty); + }); + + test('returns values on true subquery', () async { + printSeparator('True subquery test'); + var query = BookQuery()..author.where.name.like('%Rowling%'); + expect(await query.get(executor), [deathlyHallows]); + }); + }); } diff --git a/angel_orm_test/lib/src/has_many_test.dart b/angel_orm_test/lib/src/has_many_test.dart index c96add0a..17e93821 100644 --- a/angel_orm_test/lib/src/has_many_test.dart +++ b/angel_orm_test/lib/src/has_many_test.dart @@ -68,5 +68,13 @@ hasManyTests(FutureOr Function() createExecutor, var tree = await tq.deleteOne(executor); verify(tree); }); + + test('returns empty on false subquery', () async { + var tq = new TreeQuery() + ..where.id.equals(treeId) + ..fruits.where.commonName.equals('Kiwi'); + var tree = await tq.getOne(executor); + expect(tree.fruits, isEmpty); + }); }); } diff --git a/angel_orm_test/lib/src/has_one_test.dart b/angel_orm_test/lib/src/has_one_test.dart index ba1abe0a..9daeb169 100644 --- a/angel_orm_test/lib/src/has_one_test.dart +++ b/angel_orm_test/lib/src/has_one_test.dart @@ -85,4 +85,12 @@ hasOneTests(FutureOr Function() createExecutor, expect(leg.foot.id, foot.id); expect(leg.foot.nToes, foot.nToes); }); + + test('sets null on false subquery', () async { + var legQuery = new LegQuery() + ..where.id.equals(originalLeg.idAsInt) + ..foot.where.legId.equals(originalLeg.idAsInt + 1024); + var leg = await legQuery.getOne(executor); + expect(leg.foot, isNull); + }); } diff --git a/angel_orm_test/lib/src/many_to_many_test.dart b/angel_orm_test/lib/src/many_to_many_test.dart index c5588b6f..531c88e3 100644 --- a/angel_orm_test/lib/src/many_to_many_test.dart +++ b/angel_orm_test/lib/src/many_to_many_test.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:angel_orm/angel_orm.dart'; import 'package:test/test.dart'; import 'models/user.dart'; +import 'util.dart'; manyToManyTests(FutureOr Function() createExecutor, {FutureOr Function(QueryExecutor) close}) { @@ -61,6 +62,7 @@ manyToManyTests(FutureOr Function() createExecutor, print('=== THOSAKWE: ${thosakwe?.toJson()}'); // Allow thosakwe to publish... + printSeparator('Allow thosakwe to publish'); var thosakwePubQuery = RoleUserQuery(); thosakwePubQuery.values ..userId = int.parse(thosakwe.id) @@ -68,6 +70,7 @@ manyToManyTests(FutureOr Function() createExecutor, await thosakwePubQuery.insert(executor); // Allow thosakwe to subscribe... + printSeparator('Allow thosakwe to subscribe'); var thosakweSubQuery = RoleUserQuery(); thosakweSubQuery.values ..userId = int.parse(thosakwe.id) @@ -78,8 +81,8 @@ manyToManyTests(FutureOr Function() createExecutor, // await dumpQuery('select * from users;'); // await dumpQuery('select * from roles;'); // await dumpQuery('select * from role_users;'); - var query = RoleQuery()..where.id.equals(canPub.idAsInt); - await dumpQuery(query.compile(Set())); + // var query = RoleQuery()..where.id.equals(canPub.idAsInt); + // await dumpQuery(query.compile(Set())); print('\n'); print('=================================================='); @@ -95,6 +98,7 @@ manyToManyTests(FutureOr Function() createExecutor, } test('fetch roles for user', () async { + printSeparator('Fetch roles for user test'); var user = await fetchThosakwe(); expect(user.roles, hasLength(2)); expect(user.roles, contains(canPub)); @@ -108,4 +112,21 @@ manyToManyTests(FutureOr Function() createExecutor, expect(r.users.toList(), [thosakwe]); } }); + + test('only fetches linked', () async { + // Create a new user. The roles list should be empty, + // be there are no related rules. + var userQuery = UserQuery(); + userQuery.values + ..username = 'Prince' + ..password = 'Rogers' + ..email = 'Nelson'; + var user = await userQuery.insert(executor); + expect(user.roles, isEmpty); + + // Fetch again, just to be doubly sure. + var query = UserQuery()..where.id.equals(user.idAsInt); + var fetched = await query.getOne(executor); + expect(fetched.roles, isEmpty); + }); } diff --git a/angel_orm_test/lib/src/models/book.dart b/angel_orm_test/lib/src/models/book.dart index 1d515c67..b89b171d 100644 --- a/angel_orm_test/lib/src/models/book.dart +++ b/angel_orm_test/lib/src/models/book.dart @@ -9,10 +9,10 @@ part 'book.g.dart'; @serializable @orm class _Book extends Model { - @belongsTo + @BelongsTo(joinType: JoinType.inner) _Author author; - @BelongsTo(localKey: "partner_author_id") + @BelongsTo(localKey: "partner_author_id", joinType: JoinType.inner) _Author partnerAuthor; String name; diff --git a/angel_orm_test/lib/src/models/book.g.dart b/angel_orm_test/lib/src/models/book.g.dart index acbfb238..9b0d1bdf 100644 --- a/angel_orm_test/lib/src/models/book.g.dart +++ b/angel_orm_test/lib/src/models/book.g.dart @@ -51,14 +51,16 @@ class AuthorMigration extends Migration { // ************************************************************************** class BookQuery extends Query { - BookQuery({Set trampoline}) { + BookQuery({Query parent, Set trampoline}) : super(parent: parent) { trampoline ??= Set(); trampoline.add(tableName); _where = BookQueryWhere(this); - leftJoin('authors', 'author_id', 'id', + join(_author = AuthorQuery(trampoline: trampoline, parent: this), + 'author_id', 'id', additionalFields: const ['id', 'created_at', 'updated_at', 'name'], trampoline: trampoline); - leftJoin('authors', 'partner_author_id', 'id', + join(_partnerAuthor = AuthorQuery(trampoline: trampoline, parent: this), + 'partner_author_id', 'id', additionalFields: const ['id', 'created_at', 'updated_at', 'name'], trampoline: trampoline); } @@ -68,6 +70,10 @@ class BookQuery extends Query { BookQueryWhere _where; + AuthorQuery _author; + + AuthorQuery _partnerAuthor; + @override get casts { return {}; @@ -122,6 +128,14 @@ class BookQuery extends Query { deserialize(List row) { return parseRow(row); } + + AuthorQuery get author { + return _author; + } + + AuthorQuery get partnerAuthor { + return _partnerAuthor; + } } class BookQueryWhere extends QueryWhere { @@ -202,7 +216,7 @@ class BookQueryValues extends MapQueryValues { } class AuthorQuery extends Query { - AuthorQuery({Set trampoline}) { + AuthorQuery({Query parent, Set trampoline}) : super(parent: parent) { trampoline ??= Set(); trampoline.add(tableName); _where = AuthorQueryWhere(this); diff --git a/angel_orm_test/lib/src/models/car.g.dart b/angel_orm_test/lib/src/models/car.g.dart index d637ece7..9de60a6b 100644 --- a/angel_orm_test/lib/src/models/car.g.dart +++ b/angel_orm_test/lib/src/models/car.g.dart @@ -31,7 +31,7 @@ class CarMigration extends Migration { // ************************************************************************** class CarQuery extends Query { - CarQuery({Set trampoline}) { + CarQuery({Query parent, Set trampoline}) : super(parent: parent) { trampoline ??= Set(); trampoline.add(tableName); _where = CarQueryWhere(this); diff --git a/angel_orm_test/lib/src/models/email_indexed.dart b/angel_orm_test/lib/src/models/email_indexed.dart index 3b215365..676bd12e 100644 --- a/angel_orm_test/lib/src/models/email_indexed.dart +++ b/angel_orm_test/lib/src/models/email_indexed.dart @@ -1,5 +1,4 @@ import 'package:angel_migration/angel_migration.dart'; -import 'package:angel_model/angel_model.dart'; import 'package:angel_orm/angel_orm.dart'; import 'package:angel_serialize/angel_serialize.dart'; part 'email_indexed.g.dart'; diff --git a/angel_orm_test/lib/src/models/email_indexed.g.dart b/angel_orm_test/lib/src/models/email_indexed.g.dart index f8fc5422..4933ad99 100644 --- a/angel_orm_test/lib/src/models/email_indexed.g.dart +++ b/angel_orm_test/lib/src/models/email_indexed.g.dart @@ -60,11 +60,14 @@ class UserMigration extends Migration { // ************************************************************************** class RoleQuery extends Query { - RoleQuery({Set trampoline}) { + RoleQuery({Query parent, Set trampoline}) : super(parent: parent) { trampoline ??= Set(); trampoline.add(tableName); _where = RoleQueryWhere(this); - leftJoin(RoleUserQuery(trampoline: trampoline), 'role', 'role_role', + leftJoin( + '(SELECT role_users.role_role, users.email, users.name, users.password FROM users LEFT JOIN role_users ON role_users.user_email=users.email)', + 'role', + 'role_role', additionalFields: const ['email', 'name', 'password'], trampoline: trampoline); } @@ -209,13 +212,16 @@ class RoleQueryValues extends MapQueryValues { } class RoleUserQuery extends Query { - RoleUserQuery({Set trampoline}) { + RoleUserQuery({Query parent, Set trampoline}) + : super(parent: parent) { trampoline ??= Set(); trampoline.add(tableName); _where = RoleUserQueryWhere(this); - leftJoin('roles', 'role_role', 'role', + leftJoin(_role = RoleQuery(trampoline: trampoline, parent: this), + 'role_role', 'role', additionalFields: const ['role'], trampoline: trampoline); - leftJoin('users', 'user_email', 'email', + leftJoin(_user = UserQuery(trampoline: trampoline, parent: this), + 'user_email', 'email', additionalFields: const ['email', 'name', 'password'], trampoline: trampoline); } @@ -225,6 +231,10 @@ class RoleUserQuery extends Query { RoleUserQueryWhere _where; + RoleQuery _role; + + UserQuery _user; + @override get casts { return {}; @@ -268,6 +278,14 @@ class RoleUserQuery extends Query { deserialize(List row) { return parseRow(row); } + + RoleQuery get role { + return _role; + } + + UserQuery get user { + return _user; + } } class RoleUserQueryWhere extends QueryWhere { @@ -312,12 +330,16 @@ class RoleUserQueryValues extends MapQueryValues { } class UserQuery extends Query { - UserQuery({Set trampoline}) { + UserQuery({Query parent, Set trampoline}) : super(parent: parent) { trampoline ??= Set(); trampoline.add(tableName); _where = UserQueryWhere(this); - leftJoin(RoleUserQuery(trampoline: trampoline), 'email', 'user_email', - additionalFields: const ['role'], trampoline: trampoline); + leftJoin( + '(SELECT role_users.user_email, roles.role FROM roles LEFT JOIN role_users ON role_users.role_role=roles.role)', + 'email', + 'user_email', + additionalFields: const ['role'], + trampoline: trampoline); } @override diff --git a/angel_orm_test/lib/src/models/has_car.dart b/angel_orm_test/lib/src/models/has_car.dart index 6e7a7c8e..6a6e6b64 100644 --- a/angel_orm_test/lib/src/models/has_car.dart +++ b/angel_orm_test/lib/src/models/has_car.dart @@ -2,13 +2,12 @@ import 'package:angel_migration/angel_migration.dart'; import 'package:angel_model/angel_model.dart'; import 'package:angel_orm/angel_orm.dart'; import 'package:angel_serialize/angel_serialize.dart'; -import 'package:meta/meta.dart'; -import 'car.dart'; +// import 'car.dart'; part 'has_car.g.dart'; -Map _carToMap(Car car) => car.toJson(); +// Map _carToMap(Car car) => car.toJson(); -Car _carFromMap(map) => CarSerializer.fromMap(map as Map); +// Car _carFromMap(map) => CarSerializer.fromMap(map as Map); enum CarType { sedan, suv, atv } diff --git a/angel_orm_test/lib/src/models/has_car.g.dart b/angel_orm_test/lib/src/models/has_car.g.dart index 2dd9d0a2..b8adf3d5 100644 --- a/angel_orm_test/lib/src/models/has_car.g.dart +++ b/angel_orm_test/lib/src/models/has_car.g.dart @@ -28,7 +28,7 @@ class HasCarMigration extends Migration { // ************************************************************************** class HasCarQuery extends Query { - HasCarQuery({Set trampoline}) { + HasCarQuery({Query parent, Set trampoline}) : super(parent: parent) { trampoline ??= Set(); trampoline.add(tableName); _where = HasCarQueryWhere(this); diff --git a/angel_orm_test/lib/src/models/has_map.dart b/angel_orm_test/lib/src/models/has_map.dart index 8414de94..f98de5a7 100644 --- a/angel_orm_test/lib/src/models/has_map.dart +++ b/angel_orm_test/lib/src/models/has_map.dart @@ -5,8 +5,8 @@ import 'package:angel_serialize/angel_serialize.dart'; import 'package:collection/collection.dart'; part 'has_map.g.dart'; -String _boolToCustom(bool v) => v ? 'yes' : 'no'; -bool _customToBool(v) => v == 'yes'; +// String _boolToCustom(bool v) => v ? 'yes' : 'no'; +// bool _customToBool(v) => v == 'yes'; @orm @serializable diff --git a/angel_orm_test/lib/src/models/has_map.g.dart b/angel_orm_test/lib/src/models/has_map.g.dart index 77418734..71a612f7 100644 --- a/angel_orm_test/lib/src/models/has_map.g.dart +++ b/angel_orm_test/lib/src/models/has_map.g.dart @@ -26,7 +26,7 @@ class HasMapMigration extends Migration { // ************************************************************************** class HasMapQuery extends Query { - HasMapQuery({Set trampoline}) { + HasMapQuery({Query parent, Set trampoline}) : super(parent: parent) { trampoline ??= Set(); trampoline.add(tableName); _where = HasMapQueryWhere(this); diff --git a/angel_orm_test/lib/src/models/leg.g.dart b/angel_orm_test/lib/src/models/leg.g.dart index 28042af5..1575ddfb 100644 --- a/angel_orm_test/lib/src/models/leg.g.dart +++ b/angel_orm_test/lib/src/models/leg.g.dart @@ -46,11 +46,12 @@ class FootMigration extends Migration { // ************************************************************************** class LegQuery extends Query { - LegQuery({Set trampoline}) { + LegQuery({Query parent, Set trampoline}) : super(parent: parent) { trampoline ??= Set(); trampoline.add(tableName); _where = LegQueryWhere(this); - leftJoin('feet', 'id', 'leg_id', + leftJoin( + _foot = FootQuery(trampoline: trampoline, parent: this), 'id', 'leg_id', additionalFields: const [ 'id', 'created_at', @@ -66,6 +67,8 @@ class LegQuery extends Query { LegQueryWhere _where; + FootQuery _foot; + @override get casts { return {}; @@ -109,6 +112,10 @@ class LegQuery extends Query { deserialize(List row) { return parseRow(row); } + + FootQuery get foot { + return _foot; + } } class LegQueryWhere extends QueryWhere { @@ -166,7 +173,7 @@ class LegQueryValues extends MapQueryValues { } class FootQuery extends Query { - FootQuery({Set trampoline}) { + FootQuery({Query parent, Set trampoline}) : super(parent: parent) { trampoline ??= Set(); trampoline.add(tableName); _where = FootQueryWhere(this); diff --git a/angel_orm_test/lib/src/models/order.g.dart b/angel_orm_test/lib/src/models/order.g.dart index b18adb9c..edff71ca 100644 --- a/angel_orm_test/lib/src/models/order.g.dart +++ b/angel_orm_test/lib/src/models/order.g.dart @@ -49,11 +49,12 @@ class CustomerMigration extends Migration { // ************************************************************************** class OrderQuery extends Query { - OrderQuery({Set trampoline}) { + OrderQuery({Query parent, Set trampoline}) : super(parent: parent) { trampoline ??= Set(); trampoline.add(tableName); _where = OrderQueryWhere(this); - leftJoin('customers', 'customer_id', 'id', + leftJoin(_customer = CustomerQuery(trampoline: trampoline, parent: this), + 'customer_id', 'id', additionalFields: const ['id', 'created_at', 'updated_at'], trampoline: trampoline); } @@ -63,6 +64,8 @@ class OrderQuery extends Query { OrderQueryWhere _where; + CustomerQuery _customer; + @override get casts { return {}; @@ -116,6 +119,10 @@ class OrderQuery extends Query { deserialize(List row) { return parseRow(row); } + + CustomerQuery get customer { + return _customer; + } } class OrderQueryWhere extends QueryWhere { @@ -210,7 +217,8 @@ class OrderQueryValues extends MapQueryValues { } class CustomerQuery extends Query { - CustomerQuery({Set trampoline}) { + CustomerQuery({Query parent, Set trampoline}) + : super(parent: parent) { trampoline ??= Set(); trampoline.add(tableName); _where = CustomerQueryWhere(this); diff --git a/angel_orm_test/lib/src/models/tree.g.dart b/angel_orm_test/lib/src/models/tree.g.dart index 9adfb803..b20ca724 100644 --- a/angel_orm_test/lib/src/models/tree.g.dart +++ b/angel_orm_test/lib/src/models/tree.g.dart @@ -46,11 +46,12 @@ class FruitMigration extends Migration { // ************************************************************************** class TreeQuery extends Query { - TreeQuery({Set trampoline}) { + TreeQuery({Query parent, Set trampoline}) : super(parent: parent) { trampoline ??= Set(); trampoline.add(tableName); _where = TreeQueryWhere(this); - leftJoin(FruitQuery(trampoline: trampoline), 'id', 'tree_id', + leftJoin(_fruits = FruitQuery(trampoline: trampoline, parent: this), 'id', + 'tree_id', additionalFields: const [ 'id', 'created_at', @@ -66,6 +67,8 @@ class TreeQuery extends Query { TreeQueryWhere _where; + FruitQuery _fruits; + @override get casts { return {}; @@ -112,6 +115,10 @@ class TreeQuery extends Query { return parseRow(row); } + FruitQuery get fruits { + return _fruits; + } + @override get(QueryExecutor executor) { return super.get(executor).then((result) { @@ -225,7 +232,7 @@ class TreeQueryValues extends MapQueryValues { } class FruitQuery extends Query { - FruitQuery({Set trampoline}) { + FruitQuery({Query parent, Set trampoline}) : super(parent: parent) { trampoline ??= Set(); trampoline.add(tableName); _where = FruitQueryWhere(this); diff --git a/angel_orm_test/lib/src/models/unorthodox.g.dart b/angel_orm_test/lib/src/models/unorthodox.g.dart index 400d5da6..7a803db9 100644 --- a/angel_orm_test/lib/src/models/unorthodox.g.dart +++ b/angel_orm_test/lib/src/models/unorthodox.g.dart @@ -106,7 +106,8 @@ class FooPivotMigration extends Migration { // ************************************************************************** class UnorthodoxQuery extends Query { - UnorthodoxQuery({Set trampoline}) { + UnorthodoxQuery({Query parent, Set trampoline}) + : super(parent: parent) { trampoline ??= Set(); trampoline.add(tableName); _where = UnorthodoxQueryWhere(this); @@ -183,13 +184,19 @@ class UnorthodoxQueryValues extends MapQueryValues { } class WeirdJoinQuery extends Query { - WeirdJoinQuery({Set trampoline}) { + WeirdJoinQuery({Query parent, Set trampoline}) + : super(parent: parent) { trampoline ??= Set(); trampoline.add(tableName); _where = WeirdJoinQueryWhere(this); - leftJoin('unorthodoxes', 'join_name', 'name', - additionalFields: const ['name'], trampoline: trampoline); - leftJoin('songs', 'id', 'weird_join_id', + leftJoin( + _unorthodox = UnorthodoxQuery(trampoline: trampoline, parent: this), + 'join_name', + 'name', + additionalFields: const ['name'], + trampoline: trampoline); + leftJoin(_song = SongQuery(trampoline: trampoline, parent: this), 'id', + 'weird_join_id', additionalFields: const [ 'id', 'created_at', @@ -198,10 +205,15 @@ class WeirdJoinQuery extends Query { 'title' ], trampoline: trampoline); - leftJoin(NumbaQuery(trampoline: trampoline), 'id', 'parent', + leftJoin(_numbas = NumbaQuery(trampoline: trampoline, parent: this), 'id', + 'parent', additionalFields: const ['i', 'parent'], trampoline: trampoline); - leftJoin(FooPivotQuery(trampoline: trampoline), 'id', 'weird_join_id', - additionalFields: const ['bar'], trampoline: trampoline); + leftJoin( + '(SELECT foo_pivots.weird_join_id, foos.bar FROM foos LEFT JOIN foo_pivots ON foo_pivots.foo_bar=foos.bar)', + 'id', + 'weird_join_id', + additionalFields: const ['bar'], + trampoline: trampoline); } @override @@ -209,6 +221,12 @@ class WeirdJoinQuery extends Query { WeirdJoinQueryWhere _where; + UnorthodoxQuery _unorthodox; + + SongQuery _song; + + NumbaQuery _numbas; + @override get casts { return {}; @@ -265,6 +283,18 @@ class WeirdJoinQuery extends Query { return parseRow(row); } + UnorthodoxQuery get unorthodox { + return _unorthodox; + } + + SongQuery get song { + return _song; + } + + NumbaQuery get numbas { + return _numbas; + } + @override bool canCompile(trampoline) { return (!(trampoline.contains('weird_joins') && @@ -372,7 +402,7 @@ class WeirdJoinQueryValues extends MapQueryValues { } class SongQuery extends Query { - SongQuery({Set trampoline}) { + SongQuery({Query parent, Set trampoline}) : super(parent: parent) { trampoline ??= Set(); trampoline.add(tableName); _where = SongQueryWhere(this); @@ -489,7 +519,7 @@ class SongQueryValues extends MapQueryValues { } class NumbaQuery extends Query { - NumbaQuery({Set trampoline}) { + NumbaQuery({Query parent, Set trampoline}) : super(parent: parent) { trampoline ??= Set(); trampoline.add(tableName); _where = NumbaQueryWhere(this); @@ -575,12 +605,16 @@ class NumbaQueryValues extends MapQueryValues { } class FooQuery extends Query { - FooQuery({Set trampoline}) { + FooQuery({Query parent, Set trampoline}) : super(parent: parent) { trampoline ??= Set(); trampoline.add(tableName); _where = FooQueryWhere(this); - leftJoin(FooPivotQuery(trampoline: trampoline), 'bar', 'foo_bar', - additionalFields: const ['id', 'join_name'], trampoline: trampoline); + leftJoin( + '(SELECT foo_pivots.foo_bar, weird_joins.id, weird_joins.join_name FROM weird_joins LEFT JOIN foo_pivots ON foo_pivots.weird_join_id=weird_joins.id)', + 'bar', + 'foo_bar', + additionalFields: const ['id', 'join_name'], + trampoline: trampoline); } @override @@ -723,13 +757,16 @@ class FooQueryValues extends MapQueryValues { } class FooPivotQuery extends Query { - FooPivotQuery({Set trampoline}) { + FooPivotQuery({Query parent, Set trampoline}) + : super(parent: parent) { trampoline ??= Set(); trampoline.add(tableName); _where = FooPivotQueryWhere(this); - leftJoin('weird_joins', 'weird_join_id', 'id', + leftJoin(_weirdJoin = WeirdJoinQuery(trampoline: trampoline, parent: this), + 'weird_join_id', 'id', additionalFields: const ['id', 'join_name'], trampoline: trampoline); - leftJoin('foos', 'foo_bar', 'bar', + leftJoin( + _foo = FooQuery(trampoline: trampoline, parent: this), 'foo_bar', 'bar', additionalFields: const ['bar'], trampoline: trampoline); } @@ -738,6 +775,10 @@ class FooPivotQuery extends Query { FooPivotQueryWhere _where; + WeirdJoinQuery _weirdJoin; + + FooQuery _foo; + @override get casts { return {}; @@ -781,6 +822,14 @@ class FooPivotQuery extends Query { deserialize(List row) { return parseRow(row); } + + WeirdJoinQuery get weirdJoin { + return _weirdJoin; + } + + FooQuery get foo { + return _foo; + } } class FooPivotQueryWhere extends QueryWhere { diff --git a/angel_orm_test/lib/src/models/user.g.dart b/angel_orm_test/lib/src/models/user.g.dart index 19dade2e..d8ecab33 100644 --- a/angel_orm_test/lib/src/models/user.g.dart +++ b/angel_orm_test/lib/src/models/user.g.dart @@ -62,11 +62,14 @@ class RoleMigration extends Migration { // ************************************************************************** class UserQuery extends Query { - UserQuery({Set trampoline}) { + UserQuery({Query parent, Set trampoline}) : super(parent: parent) { trampoline ??= Set(); trampoline.add(tableName); _where = UserQueryWhere(this); - leftJoin(RoleUserQuery(trampoline: trampoline), 'id', 'user_id', + leftJoin( + '(SELECT role_users.user_id, roles.id, roles.created_at, roles.updated_at, roles.name FROM roles LEFT JOIN role_users ON role_users.role_id=roles.id)', + 'id', + 'user_id', additionalFields: const ['id', 'created_at', 'updated_at', 'name'], trampoline: trampoline); } @@ -268,14 +271,17 @@ class UserQueryValues extends MapQueryValues { } class RoleUserQuery extends Query { - RoleUserQuery({Set trampoline}) { + RoleUserQuery({Query parent, Set trampoline}) + : super(parent: parent) { trampoline ??= Set(); trampoline.add(tableName); _where = RoleUserQueryWhere(this); - leftJoin('roles', 'role_id', 'id', + leftJoin(_role = RoleQuery(trampoline: trampoline, parent: this), 'role_id', + 'id', additionalFields: const ['id', 'created_at', 'updated_at', 'name'], trampoline: trampoline); - leftJoin('users', 'user_id', 'id', + leftJoin(_user = UserQuery(trampoline: trampoline, parent: this), 'user_id', + 'id', additionalFields: const [ 'id', 'created_at', @@ -292,6 +298,10 @@ class RoleUserQuery extends Query { RoleUserQueryWhere _where; + RoleQuery _role; + + UserQuery _user; + @override get casts { return {}; @@ -335,6 +345,14 @@ class RoleUserQuery extends Query { deserialize(List row) { return parseRow(row); } + + RoleQuery get role { + return _role; + } + + UserQuery get user { + return _user; + } } class RoleUserQueryWhere extends QueryWhere { @@ -379,11 +397,14 @@ class RoleUserQueryValues extends MapQueryValues { } class RoleQuery extends Query { - RoleQuery({Set trampoline}) { + RoleQuery({Query parent, Set trampoline}) : super(parent: parent) { trampoline ??= Set(); trampoline.add(tableName); _where = RoleQueryWhere(this); - leftJoin(RoleUserQuery(trampoline: trampoline), 'id', 'role_id', + leftJoin( + '(SELECT role_users.role_id, users.id, users.created_at, users.updated_at, users.username, users.password, users.email FROM users LEFT JOIN role_users ON role_users.user_id=users.id)', + 'id', + 'role_id', additionalFields: const [ 'id', 'created_at', diff --git a/angel_orm_test/lib/src/util.dart b/angel_orm_test/lib/src/util.dart new file mode 100644 index 00000000..eadc05b0 --- /dev/null +++ b/angel_orm_test/lib/src/util.dart @@ -0,0 +1,12 @@ +import 'dart:io'; +import 'package:io/ansi.dart'; + +void printSeparator(String title) { + var b = StringBuffer('===' + title.toUpperCase()); + for (int i = b.length; i < stdout.terminalColumns - 3; i++) { + b.write('='); + } + for (int i = 0; i < 3; i++) { + print(magenta.wrap(b.toString())); + } +} diff --git a/angel_orm_test/pubspec.yaml b/angel_orm_test/pubspec.yaml index d22b79a4..d5dde49b 100644 --- a/angel_orm_test/pubspec.yaml +++ b/angel_orm_test/pubspec.yaml @@ -1,16 +1,20 @@ name: angel_orm_test publish_to: none -description: Common tests for Angel ORM backends.s +description: Common tests for Angel ORM backends. environment: sdk: ">=2.0.0 <3.0.0" dependencies: - angel_migration: ^2.0.0-alpha + angel_migration: + path: ../angel_migration angel_model: ^1.0.0 - angel_orm: ^2.0.0-dev + angel_orm: ^2.0.0 angel_serialize: ^2.0.0 test: ^1.0.0 dev_dependencies: angel_orm_generator: path: ../angel_orm_generator - angel_framework: ^2.0.0-alpha - build_runner: ^1.0.0 \ No newline at end of file + angel_framework: ^2.0.0 + build_runner: ^1.0.0 +dependency_overrides: + angel_orm: + path: ../angel_orm \ No newline at end of file