belongs to

This commit is contained in:
Tobe O 2018-12-03 18:13:11 -05:00
parent 207cd50afe
commit ff12ed6bc0
23 changed files with 694 additions and 135 deletions

View file

@ -1,3 +1,12 @@
# 2.0.0-dev.12
* Always apply `toSql` escapes.
# 2.0.0-dev.11
* Remove `limit(1)` except on `getOne`
# 2.0.0-dev.10
* Add `withFields` to `compile()`
# 2.0.0-dev.9
* Permanent preamble fix

View file

@ -1,5 +1,6 @@
import 'package:intl/intl.dart' show DateFormat;
import 'package:string_scanner/string_scanner.dart';
import 'query.dart';
final DateFormat dateYmd = new DateFormat('yyyy-MM-dd');
final DateFormat dateYmdHms = new DateFormat('yyyy-MM-dd HH:mm:ss');
@ -148,8 +149,8 @@ class StringSqlExpressionBuilder implements SqlExpressionBuilder<String> {
String compile() {
if (_raw != null) return _raw;
if (_value == null) return null;
var v = sanitizeExpression(_value);
return "$_op '$v'";
var v = toSql(_value);
return "$_op $v";
}
void isEmpty() => equals('');

View file

@ -15,7 +15,8 @@ abstract class QueryBase<T> {
/// A String of all [fields], joined by a comma (`,`).
String get fieldSet => fields.join(', ');
String compile({bool includeTableName: false, String preamble});
String compile(
{bool includeTableName: false, String preamble, bool withFields: true});
T deserialize(List row);
@ -54,6 +55,8 @@ String toSql(Object obj) {
} else if (obj == null) {
return 'NULL';
} else if (obj is String) {
var s = obj.replaceAll("'", "\\'");
return "'$s'";
var b = new StringBuffer();
var it = obj.runes.iterator;
@ -82,10 +85,10 @@ String toSql(Object obj) {
/// A SQL `SELECT` query builder.
abstract class Query<T, Where extends QueryWhere> extends QueryBase<T> {
final List<JoinBuilder> _joins = [];
final List<OrderBy> _orderBy = [];
String _crossJoin, _groupBy;
int _limit, _offset;
JoinBuilder _join;
/// The table against which to execute this query.
String get tableName;
@ -152,53 +155,68 @@ abstract class Query<T, Where extends QueryWhere> extends QueryBase<T> {
_crossJoin = tableName;
}
String _joinAlias() => 'a${_joins.length}';
/// Execute an `INNER JOIN` against another table.
void join(String tableName, String localKey, String foreignKey,
{String op: '='}) {
_join = new JoinBuilder(
{String op: '=', List<String> additionalFields: const []}) {
_joins.add(new JoinBuilder(
JoinType.inner, this, tableName, localKey, foreignKey,
op: op);
op: op, alias: _joinAlias(), additionalFields: additionalFields));
}
/// Execute a `LEFT JOIN` against another table.
void leftJoin(String tableName, String localKey, String foreignKey,
{String op: '='}) {
_join = new JoinBuilder(
{String op: '=', List<String> additionalFields: const []}) {
_joins.add(new JoinBuilder(
JoinType.left, this, tableName, localKey, foreignKey,
op: op);
op: op, alias: _joinAlias(), additionalFields: additionalFields));
}
/// Execute a `RIGHT JOIN` against another table.
void rightJoin(String tableName, String localKey, String foreignKey,
{String op: '='}) {
_join = new JoinBuilder(
{String op: '=', List<String> additionalFields: const []}) {
_joins.add(new JoinBuilder(
JoinType.right, this, tableName, localKey, foreignKey,
op: op);
op: op, alias: _joinAlias(), additionalFields: additionalFields));
}
/// Execute a `FULL OUTER JOIN` against another table.
void fullOuterJoin(String tableName, String localKey, String foreignKey,
{String op: '='}) {
_join = new JoinBuilder(
{String op: '=', List<String> additionalFields: const []}) {
_joins.add(new JoinBuilder(
JoinType.full, this, tableName, localKey, foreignKey,
op: op);
op: op, alias: _joinAlias(), additionalFields: additionalFields));
}
/// Execute a `SELF JOIN`.
void selfJoin(String tableName, String localKey, String foreignKey,
{String op: '='}) {
_join = new JoinBuilder(
{String op: '=', List<String> additionalFields: const []}) {
_joins.add(new JoinBuilder(
JoinType.self, this, tableName, localKey, foreignKey,
op: op);
op: op, alias: _joinAlias(), additionalFields: additionalFields));
}
@override
String compile({bool includeTableName: false, String preamble}) {
String compile(
{bool includeTableName: false, String preamble, bool withFields: true}) {
includeTableName = includeTableName || _joins.isNotEmpty;
var b = new StringBuffer(preamble ?? 'SELECT');
b.write(' ');
var f = fields ?? ['*'];
if (includeTableName) f = f.map((s) => '$tableName.$s').toList();
b.write(f.join(', '));
List<String> f;
if (fields == null) {
f = ['*'];
} else {
f = new List<String>.from(
fields.map((s) => includeTableName ? '$tableName.$s' : s));
_joins.forEach((j) {
f
..add(j.fieldName)
..addAll(j.additionalFields.map((s) => j.nameFor(s)));
});
}
if (withFields) b.write(f.join(', '));
b.write(' FROM $tableName');
var whereClause =
where.compile(tableName: includeTableName ? tableName : null);
@ -208,7 +226,7 @@ abstract class Query<T, Where extends QueryWhere> extends QueryBase<T> {
if (_groupBy != null) b.write(' GROUP BY $_groupBy');
for (var item in _orderBy) b.write(' ${item.compile()}');
if (_crossJoin != null) b.write(' CROSS JOIN $_crossJoin');
if (_join != null) b.write(' ${_join.compile()}');
for (var join in _joins) b.write(' ${join.compile()}');
return b.toString();
}
@ -219,14 +237,13 @@ abstract class Query<T, Where extends QueryWhere> extends QueryBase<T> {
}
Future<List<T>> delete(QueryExecutor executor) async {
var sql = compile(preamble: 'DELETE FROM $tableName');
var sql = compile(preamble: 'DELETE', withFields: false);
return executor
.query(sql, fields)
.then((it) => it.map(deserialize).toList());
}
Future<T> deleteOne(QueryExecutor executor) {
limit(1);
return delete(executor).then((it) => it.isEmpty ? null : it.first);
}
@ -260,7 +277,6 @@ abstract class Query<T, Where extends QueryWhere> extends QueryBase<T> {
}
Future<T> updateOne(QueryExecutor executor) {
limit(1);
return update(executor).then((it) => it.isEmpty ? null : it.first);
}
}
@ -381,7 +397,8 @@ class Union<T> extends QueryBase<T> {
T deserialize(List row) => left.deserialize(row);
@override
String compile({bool includeTableName: false, String preamble}) {
String compile(
{bool includeTableName: false, String preamble, bool withFields: true}) {
var selector = all == true ? 'UNION ALL' : 'UNION';
return '(${left.compile(includeTableName: includeTableName)}) $selector (${right.compile(includeTableName: includeTableName)})';
}
@ -391,15 +408,28 @@ class Union<T> extends QueryBase<T> {
class JoinBuilder {
final JoinType type;
final Query from;
final String to, key, value, op;
final String to, key, value, op, alias;
final List<String> additionalFields;
JoinBuilder(this.type, this.from, this.to, this.key, this.value,
{this.op: '='});
{this.op: '=', this.alias, this.additionalFields: const []});
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() {
var b = new StringBuffer();
var left = '${from.tableName}.$key';
var right = '$to.$value';
var right = fieldName;
switch (type) {
case JoinType.inner:
@ -419,7 +449,9 @@ class JoinBuilder {
break;
}
b.write(' $to ON $left$op$right');
b.write(' $to');
if (alias != null) b.write(' $alias');
b.write(' ON $left$op$right');
return b.toString();
}
}

View file

@ -50,8 +50,7 @@ class HasOne extends Relationship {
const HasOne hasOne = const HasOne();
class BelongsTo extends Relationship {
const BelongsTo(
{String localKey: 'id', String foreignKey, String foreignTable})
const BelongsTo({String localKey, String foreignKey, String foreignTable})
: super(RelationshipType.belongsTo,
localKey: localKey,
foreignKey: foreignKey,

View file

@ -1,5 +1,5 @@
name: angel_orm
version: 2.0.0-dev.9
version: 2.0.0-dev.12
description: Runtime support for Angel's ORM. Includes base classes for queries.
author: Tobe O <thosakwe@gmail.com>
homepage: https://github.com/angel-dart/orm

View file

@ -1,8 +1,11 @@
import 'dart:async';
import 'package:analyzer/dart/constant/value.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:angel_orm/angel_orm.dart';
import 'package:angel_serialize/angel_serialize.dart';
import 'package:angel_serialize_generator/angel_serialize_generator.dart';
import 'package:angel_serialize_generator/build_context.dart';
import 'package:angel_serialize_generator/context.dart';
import 'package:build/build.dart';
@ -12,6 +15,15 @@ import 'package:source_gen/source_gen.dart';
import 'readers.dart';
bool isHasRelation(Relationship r) =>
r.type == RelationshipType.hasOne || r.type == RelationshipType.hasMany;
bool isBelongsRelation(Relationship r) =>
r.type == RelationshipType.belongsTo ||
r.type == RelationshipType.belongsToMany;
final Map<Uri, OrmBuildContext> _cache = {};
Future<OrmBuildContext> buildOrmContext(
ClassElement clazz,
ConstantReader annotation,
@ -20,6 +32,21 @@ Future<OrmBuildContext> buildOrmContext(
bool autoSnakeCaseNames,
bool autoIdAndDateFields,
{bool heedExclude: true}) async {
// Check for @generatedSerializable
// ignore: unused_local_variable
DartObject generatedSerializable;
while ((generatedSerializable =
const TypeChecker.fromRuntime(GeneratedSerializable)
.firstAnnotationOf(clazz)) !=
null) {
clazz = clazz.supertype.element;
}
var uri = clazz.source.uri;
if (_cache.containsKey(uri)) {
return _cache[uri];
}
var buildCtx = await buildContext(clazz, annotation, buildStep, resolver,
autoSnakeCaseNames, autoIdAndDateFields,
heedExclude: heedExclude);
@ -30,6 +57,7 @@ Future<OrmBuildContext> buildOrmContext(
(ormAnnotation.tableName?.isNotEmpty == true)
? ormAnnotation.tableName
: pluralize(new ReCase(clazz.name).snakeCase));
_cache[uri] = ctx;
// Read all fields
for (var field in buildCtx.fields) {
@ -65,9 +93,84 @@ Future<OrmBuildContext> buildOrmContext(
);
}
if (column?.type == null)
throw 'Cannot infer SQL column type for field "${field.name}" with type "${field.type.name}".';
ctx.columns[field.name] = column;
// Try to find a relationship
var ann = relationshipTypeChecker.firstAnnotationOf(field);
if (ann != null) {
var cr = new ConstantReader(ann);
var rc = ctx.buildContext.modelClassNameRecase;
var type = cr.read('type').intValue;
var localKey = cr.peek('localKey')?.stringValue;
var foreignKey = cr.peek('foreignKey')?.stringValue;
var foreignTable = cr.peek('foreignTable')?.stringValue;
var cascadeOnDelete = cr.peek('cascadeOnDelete')?.boolValue == true;
OrmBuildContext foreign;
if (foreignTable == null) {
if (!isModelClass(field.type)) {
throw new UnsupportedError(
'Cannot apply relationship to field "${field.name}" - ${field.type.name} is not assignable to Model.');
} else {
try {
foreign = await buildOrmContext(
field.type.element as ClassElement,
new ConstantReader(const TypeChecker.fromRuntime(Serializable)
.firstAnnotationOf(field.type.element)),
buildStep,
resolver,
autoSnakeCaseNames,
autoIdAndDateFields);
var ormAnn = const TypeChecker.fromRuntime(Orm)
.firstAnnotationOf(field.type.element);
if (ormAnn != null) {
foreignTable =
new ConstantReader(ormAnn).peek('tableName')?.stringValue;
}
foreignTable ??=
pluralize(foreign.buildContext.modelClassNameRecase.snakeCase);
} on StackOverflowError {
throw new 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);
if (type == RelationshipType.hasOne || type == RelationshipType.hasMany) {
localKey ??= 'id';
foreignKey ??= '${rc.snakeCase}_id';
} else if (type == RelationshipType.belongsTo ||
type == RelationshipType.belongsToMany) {
localKey ??= '${rcc.snakeCase}_id';
foreignKey ??= 'id';
}
var relation = new Relationship(
type,
localKey: localKey,
foreignKey: foreignKey,
foreignTable: foreignTable,
cascadeOnDelete: cascadeOnDelete,
);
if (isBelongsRelation(relation)) {
var name = new ReCase(relation.localKey).camelCase;
ctx.buildContext.aliases[name] = relation.localKey;
ctx.effectiveFields.add(new RelationFieldImpl(
name, field.type.element.context.typeProvider.intType, field.name));
}
ctx.relations[field.name] = relation;
ctx.relationTypes[relation] = foreign;
} else {
if (column?.type == null)
throw 'Cannot infer SQL column type for field "${field.name}" with type "${field.type.name}".';
ctx.columns[field.name] = column;
ctx.effectiveFields.add(field);
}
}
return ctx;
@ -112,12 +215,18 @@ Column reviveColumn(ConstantReader cr) {
);
}
const TypeChecker relationshipTypeChecker =
const TypeChecker.fromRuntime(Relationship);
class OrmBuildContext {
final BuildContext buildContext;
final Orm ormAnnotation;
final String tableName;
final Map<String, Column> columns = {};
final List<FieldElement> effectiveFields = [];
final Map<String, Relationship> relations = {};
final Map<Relationship, OrmBuildContext> relationTypes = {};
OrmBuildContext(this.buildContext, this.ormAnnotation, this.tableName);
}
@ -128,3 +237,9 @@ class _ColumnType implements ColumnType {
_ColumnType(this.name);
}
class RelationFieldImpl extends ShimFieldImpl {
final String originalFieldName;
RelationFieldImpl(String name, DartType type, this.originalFieldName)
: super(name, type);
}

View file

@ -98,8 +98,11 @@ class OrmGenerator extends GeneratorForAnnotation<Orm> {
..annotations.add(refer('override'))
..type = MethodType.getter
..body = new Block((b) {
b.addExpression(
refer('${rc.pascalCase}Fields').property('allFields').returned);
var names = ctx.effectiveFields
.map((f) =>
literalString(ctx.buildContext.resolveFieldName(f.name)))
.toList();
b.addExpression(literalConstList(names).returned);
});
}));
@ -125,8 +128,9 @@ class OrmGenerator extends GeneratorForAnnotation<Orm> {
// Add deserialize()
clazz.methods.add(new Method((m) {
m
..name = 'deserialize'
..annotations.add(refer('override'))
..name = 'parseRow'
..static = true
..returns = ctx.buildContext.modelClassType
..requiredParameters.add(new Parameter((b) => b
..name = 'row'
..type = refer('List')))
@ -134,23 +138,95 @@ class OrmGenerator extends GeneratorForAnnotation<Orm> {
int i = 0;
var args = <String, Expression>{};
for (var field in ctx.buildContext.fields) {
for (var field in ctx.effectiveFields) {
Reference type = convertTypeReference(field.type);
if (isSpecialId(field)) type = refer('int');
var expr = (refer('row').index(literalNum(i++)));
if (isSpecialId(field))
expr = expr.property('toString').call([]);
else if (field is RelationFieldImpl)
continue;
else
expr = expr.asA(type);
args[field.name] = expr;
}
b.addExpression(
ctx.buildContext.modelClassType.newInstance([], args).returned);
b.addExpression(ctx.buildContext.modelClassType
.newInstance([], args).assignVar('model'));
ctx.relations.forEach((name, relation) {
var foreign = ctx.relationTypes[relation];
var skipToList = refer('row')
.property('skip')
.call([literalNum(i)])
.property('toList')
.call([]);
var parsed = refer(
'${foreign.buildContext.modelClassNameRecase.pascalCase}Query')
.property('parseRow')
.call([skipToList]);
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 ifStr = 'if (row.length > $i) { $blockStr }';
b.statements.add(new Code(ifStr));
i += ctx.relationTypes[relation].effectiveFields.length;
});
b.addExpression(refer('model').returned);
});
}));
clazz.methods.add(new Method((m) {
m
..name = 'deserialize'
..annotations.add(refer('override'))
..requiredParameters.add(new Parameter((b) => b
..name = 'row'
..type = refer('List')))
..body = new Block((b) {
b.addExpression(refer('parseRow').call([refer('row')]).returned);
});
}));
// If there are any relations, we need some overrides.
if (ctx.relations.isNotEmpty) {
clazz.methods.add(new Method((b) {
b
..name = 'get'
..annotations.add(refer('override'))
..requiredParameters.add(new Parameter((b) => b..name = 'executor'))
..body = new Block((b) {
ctx.relations.forEach((fieldName, relation) {
//var name = ctx.buildContext.resolveFieldName(fieldName);
if (relation.type == RelationshipType.belongsTo) {
var foreign = ctx.relationTypes[relation];
var additionalFields = foreign.effectiveFields
.where((f) => f.name != 'id' || !isSpecialId(f))
.map((f) => literalString(
foreign.buildContext.resolveFieldName(f.name)));
var joinArgs = [
relation.foreignTable,
relation.localKey,
relation.foreignKey
].map(literalString);
b.addExpression(refer('leftJoin').call(joinArgs, {
'additionalFields':
literalConstList(additionalFields.toList())
}));
}
});
b.addExpression(refer('super')
.property('get')
.call([refer('executor')]).returned);
});
}));
}
});
}
@ -172,14 +248,15 @@ class OrmGenerator extends GeneratorForAnnotation<Orm> {
..annotations.add(refer('override'))
..type = MethodType.getter
..body = new Block((b) {
var references = ctx.buildContext.fields.map((f) => refer(f.name));
var references = ctx.effectiveFields.map((f) => refer(f.name));
b.addExpression(literalList(references).returned);
});
}));
// Add builders for each field
for (var field in ctx.buildContext.fields) {
for (var field in ctx.effectiveFields) {
// TODO: Handle fields with relations
var name = field.name;
Reference builderType;
if (const TypeChecker.fromRuntime(int).isExactlyType(field.type) ||
@ -197,6 +274,16 @@ class OrmGenerator extends GeneratorForAnnotation<Orm> {
} else if (const TypeChecker.fromRuntime(DateTime)
.isExactlyType(field.type)) {
builderType = refer('DateTimeSqlExpressionBuilder');
} else if (ctx.relations.containsKey(field.name)) {
var relation = ctx.relations[field.name];
if (!isBelongsRelation(relation))
continue;
else {
builderType = new TypeReference((b) => b
..symbol = 'NumericSqlExpressionBuilder'
..types.add(refer('int')));
name = relation.localKey;
}
} else {
throw new UnsupportedError(
'Cannot generate ORM code for field of type ${field.type.name}.');
@ -204,7 +291,7 @@ class OrmGenerator extends GeneratorForAnnotation<Orm> {
clazz.fields.add(new Field((b) {
b
..name = field.name
..name = name
..modifier = FieldModifier.final$
..type = builderType
..assignment = builderType.newInstance([
@ -223,7 +310,7 @@ class OrmGenerator extends GeneratorForAnnotation<Orm> {
..extend = refer('MapQueryValues');
// Each field generates a getter for setter
for (var field in ctx.buildContext.fields) {
for (var field in ctx.effectiveFields) {
var name = ctx.buildContext.resolveFieldName(field.name);
var type = isSpecialId(field)
? refer('int')
@ -264,14 +351,34 @@ class OrmGenerator extends GeneratorForAnnotation<Orm> {
..body = new Block((b) {
var args = <String, Expression>{};
for (var field in ctx.buildContext.fields) {
if (isSpecialId(field)) continue;
for (var field in ctx.effectiveFields) {
if (isSpecialId(field) || field is RelationFieldImpl) continue;
args[ctx.buildContext.resolveFieldName(field.name)] =
refer('model').property(field.name);
}
b.addExpression(
refer('values').property('addAll').call([literalMap(args)]));
for (var field in ctx.effectiveFields) {
if (field is RelationFieldImpl) {
var original = field.originalFieldName;
var prop = refer('model').property(original);
// Add only if present
var target = refer('values').index(literalString(
ctx.buildContext.resolveFieldName(field.name)));
var parsedId = (refer('int')
.property('parse')
.call([prop.property('id')]));
var cond = prop.notEqualTo(literalNull);
var condStr = cond.accept(new DartEmitter());
var blkStr =
new Block((b) => b.addExpression(target.assign(parsedId)))
.accept(new DartEmitter());
var ifStmt = new Code('if ($condStr) { $blkStr }');
b.statements.add(ifStmt);
}
}
});
}));
});

View file

@ -6,7 +6,8 @@ homepage: https://github.com/angel-dart/orm
environment:
sdk: ">=2.0.0-dev <3.0.0"
dependencies:
angel_orm: ^2.0.0-dev
angel_orm:
path: ../angel_orm
angel_serialize_generator: ^2.0.0
build: ">=0.12.0 <2.0.0"
build_config: ^0.3.0

View file

@ -7,31 +7,37 @@ import 'models/book.dart';
import 'common.dart';
main() {
PostgresExecutor connection;
Author rowling;
PostgresExecutor executor;
Author jkRowling;
Author jameson;
Book deathlyHallows;
setUp(() async {
connection = await connectToPostgres(['author', 'book']);
executor = await connectToPostgres(['author', 'book']);
// Insert an author
rowling = await AuthorQuery.insert(connection, name: 'J.K. Rowling');
jameson = await AuthorQuery.insert(connection, name: 'J.K. Jameson');
var query = new AuthorQuery()..values.name = 'J.K. Rowling';
jkRowling = await query.insert(executor);
query.values.name = 'J.K. Jameson';
jameson = await query.insert(executor);
// And a book
deathlyHallows = await BookQuery.insert(connection,
authorId: int.parse(rowling.id),
name: 'Deathly Hallows',
partnerAuthorId: int.parse(jameson.id));
var bookQuery = new BookQuery();
bookQuery.values
..authorId = int.parse(jkRowling.id)
..partnerAuthorId = int.parse(jameson.id)
..name = 'Deathly Hallows';
deathlyHallows = await bookQuery.insert(executor);
});
tearDown(() => connection.close());
tearDown(() => executor.close());
group('selects', () {
test('select all', () async {
var query = new BookQuery();
var books = await query.get(connection).toList();
var books = await query.get(executor);
expect(books, hasLength(1));
var book = books.first;
@ -39,36 +45,35 @@ main() {
expect(book.id, deathlyHallows.id);
expect(book.name, deathlyHallows.name);
var author = book.author as Author;
var author = book.author;
print(author.toJson());
expect(author.id, rowling.id);
expect(author.name, rowling.name);
expect(author.id, jkRowling.id);
expect(author.name, jkRowling.name);
});
test('select one', () async {
var query = new BookQuery();
query.where.id.equals(int.parse(deathlyHallows.id));
print(query.toSql());
print(query.compile());
var book =
await BookQuery.getOne(int.parse(deathlyHallows.id), connection);
var book = await query.getOne(executor);
print(book.toJson());
expect(book.id, deathlyHallows.id);
expect(book.name, deathlyHallows.name);
var author = book.author as Author;
var author = book.author;
print(author.toJson());
expect(author.id, rowling.id);
expect(author.name, rowling.name);
expect(author.id, jkRowling.id);
expect(author.name, jkRowling.name);
});
test('where clause', () async {
var query = new BookQuery()
..where.name.equals('Goblet of Fire')
..or(new BookQueryWhere()..authorId.equals(int.parse(rowling.id)));
print(query.toSql());
..orWhere((w) => w.authorId.equals(int.parse(jkRowling.id)));
print(query.compile());
var books = await query.get(connection).toList();
var books = await query.get(executor);
expect(books, hasLength(1));
var book = books.first;
@ -76,10 +81,10 @@ main() {
expect(book.id, deathlyHallows.id);
expect(book.name, deathlyHallows.name);
var author = book.author as Author;
var author = book.author;
print(author.toJson());
expect(author.id, rowling.id);
expect(author.name, rowling.name);
expect(author.id, jkRowling.id);
expect(author.name, jkRowling.name);
});
test('union', () async {
@ -90,9 +95,9 @@ main() {
query1
..union(query2)
..unionAll(query3);
print(query1.toSql());
print(query1.compile());
var books = await query1.get(connection).toList();
var books = await query1.get(executor);
expect(books, hasLength(1));
var book = books.first;
@ -100,36 +105,38 @@ main() {
expect(book.id, deathlyHallows.id);
expect(book.name, deathlyHallows.name);
var author = book.author as Author;
var author = book.author;
print(author.toJson());
expect(author.id, rowling.id);
expect(author.name, rowling.name);
expect(author.id, jkRowling.id);
expect(author.name, jkRowling.name);
});
});
test('insert sets relationship', () {
expect(deathlyHallows.author, isNotNull);
expect((deathlyHallows.author).name, rowling.name);
expect(deathlyHallows.author, jkRowling);
//expect(deathlyHallows.author, isNotNull);
//expect(deathlyHallows.author.name, rowling.name);
});
test('delete stream', () async {
var query = new BookQuery()..where.name.equals(deathlyHallows.name);
print(query.toSql());
var books = await query.delete(connection).toList();
print(query.compile());
var books = await query.delete(executor);
expect(books, hasLength(1));
var book = books.first;
expect(book.id, deathlyHallows.id);
expect(book.author, isNotNull);
expect((book.author).name, rowling.name);
expect((book.author).name, jkRowling.name);
});
test('update book', () async {
var cloned = deathlyHallows.clone()..name = 'Sorcerer\'s Stone';
var book = await BookQuery.updateBook(connection, cloned);
var cloned = deathlyHallows.copyWith(name: "Sorcerer's Stone");
var query = new BookQuery()..values.copyFrom(cloned);
var book = await query.updateOne(executor);
print(book.toJson());
expect(book.name, cloned.name);
expect(book.author, isNotNull);
expect((book.author as Author).name, rowling.name);
expect(book.author.name, jkRowling.name);
});
}

View file

@ -0,0 +1,6 @@
CREATE TEMPORARY TABLE "authors" (
id serial PRIMARY KEY,
name varchar(255) UNIQUE NOT NULL,
created_at timestamp,
updated_at timestamp
);

View file

@ -0,0 +1,8 @@
CREATE TEMPORARY TABLE "books" (
id serial PRIMARY KEY,
author_id int NOT NULL,
partner_author_id int,
name varchar(255),
created_at timestamp,
updated_at timestamp
);

View file

@ -20,7 +20,7 @@ class AuthorQuery extends Query<Author, AuthorQueryWhere> {
@override
get fields {
return AuthorFields.allFields;
return const ['id', 'name', 'created_at', 'updated_at'];
}
@override
@ -28,13 +28,18 @@ class AuthorQuery extends Query<Author, AuthorQueryWhere> {
return new AuthorQueryWhere();
}
@override
deserialize(List row) {
return new Author(
static Author parseRow(List row) {
var model = new Author(
id: row[0].toString(),
name: (row[1] as String),
createdAt: (row[2] as DateTime),
updatedAt: (row[3] as DateTime));
return model;
}
@override
deserialize(List row) {
return parseRow(row);
}
}

View file

@ -16,6 +16,5 @@ class _Book extends Model {
@BelongsTo(localKey: "partner_author_id")
Author partnerAuthor;
int authorId;
String name;
}

View file

@ -2,6 +2,142 @@
part of angel_orm.generator.models.book;
// **************************************************************************
// OrmGenerator
// **************************************************************************
class BookQuery extends Query<Book, BookQueryWhere> {
@override
final BookQueryValues values = new BookQueryValues();
@override
final BookQueryWhere where = new BookQueryWhere();
@override
get tableName {
return 'books';
}
@override
get fields {
return const [
'id',
'author_id',
'partner_author_id',
'name',
'created_at',
'updated_at'
];
}
@override
BookQueryWhere newWhereClause() {
return new BookQueryWhere();
}
static Book parseRow(List row) {
var model = new Book(
id: row[0].toString(),
name: (row[3] as String),
createdAt: (row[4] as DateTime),
updatedAt: (row[5] as DateTime));
if (row.length > 6) {
model =
model.copyWith(author: AuthorQuery.parseRow(row.skip(6).toList()));
}
if (row.length > 10) {
model = model.copyWith(
partnerAuthor: AuthorQuery.parseRow(row.skip(10).toList()));
}
return model;
}
@override
deserialize(List row) {
return parseRow(row);
}
@override
get(executor) {
leftJoin('authors', 'author_id', 'id',
additionalFields: const ['name', 'created_at', 'updated_at']);
leftJoin('authors', 'partner_author_id', 'id',
additionalFields: const ['name', 'created_at', 'updated_at']);
return super.get(executor);
}
}
class BookQueryWhere extends QueryWhere {
final NumericSqlExpressionBuilder<int> id =
new NumericSqlExpressionBuilder<int>('id');
final NumericSqlExpressionBuilder<int> authorId =
new NumericSqlExpressionBuilder<int>('author_id');
final NumericSqlExpressionBuilder<int> partnerAuthorId =
new NumericSqlExpressionBuilder<int>('partner_author_id');
final StringSqlExpressionBuilder name =
new StringSqlExpressionBuilder('name');
final DateTimeSqlExpressionBuilder createdAt =
new DateTimeSqlExpressionBuilder('created_at');
final DateTimeSqlExpressionBuilder updatedAt =
new DateTimeSqlExpressionBuilder('updated_at');
@override
get expressionBuilders {
return [id, authorId, partnerAuthorId, name, createdAt, updatedAt];
}
}
class BookQueryValues extends MapQueryValues {
int get id {
return (values['id'] as int);
}
void set id(int value) => values['id'] = value;
int get authorId {
return (values['author_id'] as int);
}
void set authorId(int value) => values['author_id'] = value;
int get partnerAuthorId {
return (values['partner_author_id'] as int);
}
void set partnerAuthorId(int value) => values['partner_author_id'] = value;
String get name {
return (values['name'] as String);
}
void set name(String value) => values['name'] = value;
DateTime get createdAt {
return (values['created_at'] as DateTime);
}
void set createdAt(DateTime value) => values['created_at'] = value;
DateTime get updatedAt {
return (values['updated_at'] as DateTime);
}
void set updatedAt(DateTime value) => values['updated_at'] = value;
void copyFrom(Book model) {
values.addAll({
'name': model.name,
'created_at': model.createdAt,
'updated_at': model.updatedAt
});
if (model.author != null) {
values['author_id'] = int.parse(model.author.id);
}
if (model.partnerAuthor != null) {
values['partner_author_id'] = int.parse(model.partnerAuthor.id);
}
}
}
// **************************************************************************
// JsonModelGenerator
// **************************************************************************
@ -12,7 +148,6 @@ class Book extends _Book {
{this.id,
this.author,
this.partnerAuthor,
this.authorId,
this.name,
this.createdAt,
this.updatedAt});
@ -26,9 +161,6 @@ class Book extends _Book {
@override
final Author partnerAuthor;
@override
final int authorId;
@override
final String name;
@ -42,7 +174,6 @@ class Book extends _Book {
{String id,
Author author,
Author partnerAuthor,
int authorId,
String name,
DateTime createdAt,
DateTime updatedAt}) {
@ -50,7 +181,6 @@ class Book extends _Book {
id: id ?? this.id,
author: author ?? this.author,
partnerAuthor: partnerAuthor ?? this.partnerAuthor,
authorId: authorId ?? this.authorId,
name: name ?? this.name,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt);
@ -61,7 +191,6 @@ class Book extends _Book {
other.id == id &&
other.author == author &&
other.partnerAuthor == partnerAuthor &&
other.authorId == authorId &&
other.name == name &&
other.createdAt == createdAt &&
other.updatedAt == updatedAt;
@ -69,8 +198,7 @@ class Book extends _Book {
@override
int get hashCode {
return hashObjects(
[id, author, partnerAuthor, authorId, name, createdAt, updatedAt]);
return hashObjects([id, author, partnerAuthor, name, createdAt, updatedAt]);
}
Map<String, dynamic> toJson() {

View file

@ -16,7 +16,6 @@ abstract class BookSerializer {
partnerAuthor: map['partner_author'] != null
? AuthorSerializer.fromMap(map['partner_author'] as Map)
: null,
authorId: map['author_id'] as int,
name: map['name'] as String,
createdAt: map['created_at'] != null
? (map['created_at'] is DateTime
@ -38,7 +37,6 @@ abstract class BookSerializer {
'id': model.id,
'author': AuthorSerializer.toMap(model.author),
'partner_author': AuthorSerializer.toMap(model.partnerAuthor),
'author_id': model.authorId,
'name': model.name,
'created_at': model.createdAt?.toIso8601String(),
'updated_at': model.updatedAt?.toIso8601String()
@ -51,7 +49,6 @@ abstract class BookFields {
id,
author,
partnerAuthor,
authorId,
name,
createdAt,
updatedAt
@ -63,8 +60,6 @@ abstract class BookFields {
static const String partnerAuthor = 'partner_author';
static const String authorId = 'author_id';
static const String name = 'name';
static const String createdAt = 'created_at';

View file

@ -20,7 +20,15 @@ class CarQuery extends Query<Car, CarQueryWhere> {
@override
get fields {
return CarFields.allFields;
return const [
'id',
'make',
'description',
'family_friendly',
'recalled_at',
'created_at',
'updated_at'
];
}
@override
@ -28,9 +36,8 @@ class CarQuery extends Query<Car, CarQueryWhere> {
return new CarQueryWhere();
}
@override
deserialize(List row) {
return new Car(
static Car parseRow(List row) {
var model = new Car(
id: row[0].toString(),
make: (row[1] as String),
description: (row[2] as String),
@ -38,6 +45,12 @@ class CarQuery extends Query<Car, CarQueryWhere> {
recalledAt: (row[4] as DateTime),
createdAt: (row[5] as DateTime),
updatedAt: (row[6] as DateTime));
return model;
}
@override
deserialize(List row) {
return parseRow(row);
}
}

View file

@ -20,7 +20,7 @@ class CustomerQuery extends Query<Customer, CustomerQueryWhere> {
@override
get fields {
return CustomerFields.allFields;
return const ['id', 'created_at', 'updated_at'];
}
@override
@ -28,12 +28,17 @@ class CustomerQuery extends Query<Customer, CustomerQueryWhere> {
return new CustomerQueryWhere();
}
@override
deserialize(List row) {
return new Customer(
static Customer parseRow(List row) {
var model = new Customer(
id: row[0].toString(),
createdAt: (row[1] as DateTime),
updatedAt: (row[2] as DateTime));
return model;
}
@override
deserialize(List row) {
return parseRow(row);
}
}

View file

@ -20,7 +20,7 @@ class FootQuery extends Query<Foot, FootQueryWhere> {
@override
get fields {
return FootFields.allFields;
return const ['id', 'leg_id', 'n_toes', 'created_at', 'updated_at'];
}
@override
@ -28,14 +28,19 @@ class FootQuery extends Query<Foot, FootQueryWhere> {
return new FootQueryWhere();
}
@override
deserialize(List row) {
return new Foot(
static Foot parseRow(List row) {
var model = new Foot(
id: row[0].toString(),
legId: (row[1] as int),
nToes: (row[2] as int),
createdAt: (row[3] as DateTime),
updatedAt: (row[4] as DateTime));
return model;
}
@override
deserialize(List row) {
return parseRow(row);
}
}

View file

@ -20,7 +20,7 @@ class FruitQuery extends Query<Fruit, FruitQueryWhere> {
@override
get fields {
return FruitFields.allFields;
return const ['id', 'tree_id', 'common_name', 'created_at', 'updated_at'];
}
@override
@ -28,14 +28,19 @@ class FruitQuery extends Query<Fruit, FruitQueryWhere> {
return new FruitQueryWhere();
}
@override
deserialize(List row) {
return new Fruit(
static Fruit parseRow(List row) {
var model = new Fruit(
id: row[0].toString(),
treeId: (row[1] as int),
commonName: (row[2] as String),
createdAt: (row[3] as DateTime),
updatedAt: (row[4] as DateTime));
return model;
}
@override
deserialize(List row) {
return parseRow(row);
}
}

View file

@ -2,6 +2,104 @@
part of angel_orm_generator.test.models.leg;
// **************************************************************************
// OrmGenerator
// **************************************************************************
class LegQuery extends Query<Leg, LegQueryWhere> {
@override
final LegQueryValues values = new LegQueryValues();
@override
final LegQueryWhere where = new LegQueryWhere();
@override
get tableName {
return 'legs';
}
@override
get fields {
return const ['id', 'name', 'created_at', 'updated_at'];
}
@override
LegQueryWhere newWhereClause() {
return new LegQueryWhere();
}
static Leg parseRow(List row) {
var model = new Leg(
id: row[0].toString(),
name: (row[1] as String),
createdAt: (row[2] as DateTime),
updatedAt: (row[3] as DateTime));
if (row.length > 4) {
model = model.copyWith(foot: FootQuery.parseRow(row.skip(4).toList()));
}
return model;
}
@override
deserialize(List row) {
return parseRow(row);
}
@override
get(executor) {
return super.get(executor);
}
}
class LegQueryWhere extends QueryWhere {
final NumericSqlExpressionBuilder<int> id =
new NumericSqlExpressionBuilder<int>('id');
final StringSqlExpressionBuilder name =
new StringSqlExpressionBuilder('name');
final DateTimeSqlExpressionBuilder createdAt =
new DateTimeSqlExpressionBuilder('created_at');
final DateTimeSqlExpressionBuilder updatedAt =
new DateTimeSqlExpressionBuilder('updated_at');
@override
get expressionBuilders {
return [id, name, createdAt, updatedAt];
}
}
class LegQueryValues extends MapQueryValues {
int get id {
return (values['id'] as int);
}
void set id(int value) => values['id'] = value;
String get name {
return (values['name'] as String);
}
void set name(String value) => values['name'] = value;
DateTime get createdAt {
return (values['created_at'] as DateTime);
}
void set createdAt(DateTime value) => values['created_at'] = value;
DateTime get updatedAt {
return (values['updated_at'] as DateTime);
}
void set updatedAt(DateTime value) => values['updated_at'] = value;
void copyFrom(Leg model) {
values.addAll({
'name': model.name,
'created_at': model.createdAt,
'updated_at': model.updatedAt
});
}
}
// **************************************************************************
// JsonModelGenerator
// **************************************************************************

View file

@ -20,7 +20,15 @@ class OrderQuery extends Query<Order, OrderQueryWhere> {
@override
get fields {
return OrderFields.allFields;
return const [
'id',
'customer_id',
'employee_id',
'order_date',
'shipper_id',
'created_at',
'updated_at'
];
}
@override
@ -28,9 +36,8 @@ class OrderQuery extends Query<Order, OrderQueryWhere> {
return new OrderQueryWhere();
}
@override
deserialize(List row) {
return new Order(
static Order parseRow(List row) {
var model = new Order(
id: row[0].toString(),
customerId: (row[1] as int),
employeeId: (row[2] as int),
@ -38,6 +45,12 @@ class OrderQuery extends Query<Order, OrderQueryWhere> {
shipperId: (row[4] as int),
createdAt: (row[5] as DateTime),
updatedAt: (row[6] as DateTime));
return model;
}
@override
deserialize(List row) {
return parseRow(row);
}
}

View file

@ -20,7 +20,7 @@ class RoleQuery extends Query<Role, RoleQueryWhere> {
@override
get fields {
return RoleFields.allFields;
return const ['id', 'name', 'created_at', 'updated_at'];
}
@override
@ -28,13 +28,18 @@ class RoleQuery extends Query<Role, RoleQueryWhere> {
return new RoleQueryWhere();
}
@override
deserialize(List row) {
return new Role(
static Role parseRow(List row) {
var model = new Role(
id: row[0].toString(),
name: (row[1] as String),
createdAt: (row[2] as DateTime),
updatedAt: (row[3] as DateTime));
return model;
}
@override
deserialize(List row) {
return parseRow(row);
}
}

View file

@ -155,11 +155,14 @@ main() {
test('insert', () async {
var recalledAt = new DateTime.now();
var query = new CarQuery();
var now = new DateTime.now();
query.values
..make = 'Honda'
..description = 'Hello'
..familyFriendly = true
..recalledAt = recalledAt;
..recalledAt = recalledAt
..createdAt = now
..updatedAt = now;
var car = await query.insert(connection);
expect(car.id, isNotNull);
expect(car.make, 'Honda');