diff --git a/.idea/runConfigurations/tests_in_book_test_dart.xml b/.idea/runConfigurations/tests_in_book_test_dart.xml new file mode 100644 index 00000000..10d71dd0 --- /dev/null +++ b/.idea/runConfigurations/tests_in_book_test_dart.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/angel_orm/CHANGELOG.md b/angel_orm/CHANGELOG.md index 58829678..f7f4c220 100644 --- a/angel_orm/CHANGELOG.md +++ b/angel_orm/CHANGELOG.md @@ -1,3 +1,7 @@ +# 1.0.0-alpha+6 +* `DateTimeSqlExpressionBuilder` will no longer automatically +insert quotation marks around names. + # 1.0.0-alpha+5 * Corrected a typo that was causing the aforementioned test failures. `==` becomes `=`. diff --git a/angel_orm/lib/src/query.dart b/angel_orm/lib/src/query.dart index 0d461a90..904ee918 100644 --- a/angel_orm/lib/src/query.dart +++ b/angel_orm/lib/src/query.dart @@ -260,7 +260,7 @@ class DateTimeSqlExpressionBuilder implements SqlExpressionBuilder { bool _change(String _op, DateTime dt, bool time) { var dateString = time ? DATE_YMD_HMS.format(dt) : DATE_YMD.format(dt); - _raw = '"$columnName" $_op \'$dateString\''; + _raw = '$columnName $_op \'$dateString\''; return true; } @@ -291,40 +291,40 @@ class DateTimeSqlExpressionBuilder implements SqlExpressionBuilder { @override void isIn(@checked Iterable values) { - _raw = '"$columnName" IN (' + - values.map(DATE_YMD_HMS.format).map((s) => "'$s'").join(', ') + + _raw = '$columnName IN (' + + values.map(DATE_YMD_HMS.format).map((s) => '$s').join(', ') + ')'; } @override void isNotIn(@checked Iterable values) { - _raw = '"$columnName" NOT IN (' + - values.map(DATE_YMD_HMS.format).map((s) => "'$s'").join(', ') + + _raw = '$columnName NOT IN (' + + values.map(DATE_YMD_HMS.format).map((s) => '$s').join(', ') + ')'; } @override void isBetween(@checked DateTime lower, @checked DateTime upper) { var l = DATE_YMD_HMS.format(lower), u = DATE_YMD_HMS.format(upper); - _raw = "\"$columnName\" BETWEEN '$l' and '$u'"; + _raw = "$columnName BETWEEN '$l' and '$u'"; } @override void isNotBetween(@checked DateTime lower, @checked DateTime upper) { var l = DATE_YMD_HMS.format(lower), u = DATE_YMD_HMS.format(upper); - _raw = "\"$columnName\" NOT BETWEEN '$l' and '$u'"; + _raw = "$columnName NOT BETWEEN '$l' and '$u'"; } @override String compile() { if (_raw?.isNotEmpty == true) return _raw; List parts = []; - if (year.hasValue) parts.add('YEAR("$columnName") ${year.compile()}'); - if (month.hasValue) parts.add('MONTH("$columnName") ${month.compile()}'); - if (day.hasValue) parts.add('DAY("$columnName") ${day.compile()}'); - if (hour.hasValue) parts.add('HOUR("$columnName") ${hour.compile()}'); - if (minute.hasValue) parts.add('MINUTE("$columnName") ${minute.compile()}'); - if (second.hasValue) parts.add('SECOND("$columnName") ${second.compile()}'); + if (year.hasValue) parts.add('YEAR($columnName) ${year.compile()}'); + if (month.hasValue) parts.add('MONTH($columnName) ${month.compile()}'); + if (day.hasValue) parts.add('DAY($columnName) ${day.compile()}'); + if (hour.hasValue) parts.add('HOUR($columnName) ${hour.compile()}'); + if (minute.hasValue) parts.add('MINUTE($columnName) ${minute.compile()}'); + if (second.hasValue) parts.add('SECOND($columnName) ${second.compile()}'); return parts.isEmpty ? null : parts.join(' AND '); } diff --git a/angel_orm/pubspec.yaml b/angel_orm/pubspec.yaml index ba343391..9cbc0b23 100644 --- a/angel_orm/pubspec.yaml +++ b/angel_orm/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_orm -version: 1.0.0-alpha+5 +version: 1.0.0-alpha+6 description: Runtime support for Angel's ORM. author: Tobe O homepage: https://github.com/angel-dart/orm diff --git a/angel_orm_generator/CHANGELOG.md b/angel_orm_generator/CHANGELOG.md index ae80dcb7..381d02fa 100644 --- a/angel_orm_generator/CHANGELOG.md +++ b/angel_orm_generator/CHANGELOG.md @@ -1,3 +1,6 @@ +# 1.0.0-alpha+2 +* Added support for `belongsTo` relationships. Still missing `hasOne`, `hasMany`, `belongsToMany`. + # 1.0.0-alpha+1 * Closed #12. `insertX` and `updateX` now use `rc.camelCase`, instead of `rc.snakeCase`. * Closed #13. Added `limit` and `offset` properties to `XQuery`. diff --git a/angel_orm_generator/lib/src/builder/postgres/build_context.dart b/angel_orm_generator/lib/src/builder/postgres/build_context.dart index 9efe8a12..390c6140 100644 --- a/angel_orm_generator/lib/src/builder/postgres/build_context.dart +++ b/angel_orm_generator/lib/src/builder/postgres/build_context.dart @@ -1,4 +1,6 @@ import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:analyzer/src/dart/element/element.dart'; import 'package:angel_orm/angel_orm.dart'; import 'package:angel_serialize_generator/src/find_annotation.dart'; import 'package:angel_serialize_generator/build_context.dart' as serialize; @@ -21,7 +23,9 @@ PostgresBuildContext buildContext( var ctx = new PostgresBuildContext(raw, annotation, resolver, buildStep, tableName: annotation.tableName?.isNotEmpty == true ? annotation.tableName - : pluralize(new ReCase(clazz.name).snakeCase)); + : pluralize(new ReCase(clazz.name).snakeCase), + autoSnakeCaseNames: autoSnakeCaseNames != false, + autoIdAndDateFields: autoIdAndDateFields != false); var relations = new TypeChecker.fromRuntime(Relationship); List fieldNames = []; List fields = []; @@ -107,5 +111,28 @@ PostgresBuildContext buildContext( } ctx.fields.addAll(fields); + + // Add belongs to fields + // TODO: Do this for belongs to many as well + ctx.relationships.forEach((name, r) { + var relationship = ctx.populateRelationship(name); + var rc = new ReCase(relationship.localKey); + + if (relationship.type == RelationshipType.BELONGS_TO) { + var field = new RelationshipConstraintField( + rc.camelCase, ctx.typeProvider.intType, name); + ctx.fields.add(field); + ctx.aliases[field.name] = relationship.localKey; + } + }); + return ctx; } + +class RelationshipConstraintField extends FieldElementImpl { + @override + final DartType type; + final String originalName; + RelationshipConstraintField(String name, this.type, this.originalName) + : super(name, -1); +} diff --git a/angel_orm_generator/lib/src/builder/postgres/migration.dart b/angel_orm_generator/lib/src/builder/postgres/migration.dart index b6461361..0bb04055 100644 --- a/angel_orm_generator/lib/src/builder/postgres/migration.dart +++ b/angel_orm_generator/lib/src/builder/postgres/migration.dart @@ -2,6 +2,8 @@ import 'dart:async'; import 'package:analyzer/dart/element/element.dart'; import 'package:angel_orm/angel_orm.dart'; import 'package:build/build.dart'; +import 'package:inflection/inflection.dart'; +import 'package:recase/recase.dart'; import 'package:source_gen/src/annotation.dart'; import 'package:source_gen/src/utils.dart'; import 'build_context.dart'; @@ -93,6 +95,56 @@ class SQLMigrationGenerator implements Builder { if (col.nullable != true) buf.write(' NOT NULLABLE'); }); + // Relations + ctx.relationshipFields.forEach((f) { + if (i++ > 0) buf.writeln(','); + var typeName = + f.type.name.startsWith('_') ? f.type.name.substring(1) : f.type.name; + var rc = new ReCase(typeName); + var relationship = ctx.relationships[f.name]; + + if (relationship.type == RelationshipType.BELONGS_TO) { + var localKey = relationship.localKey ?? + (autoSnakeCaseNames != false + ? '${rc.snakeCase}_id' + : '${typeName}Id'); + var foreignKey = relationship.foreignKey ?? 'id'; + var foreignTable = relationship.foreignTable ?? + (autoSnakeCaseNames != false + ? pluralize(rc.snakeCase) + : pluralize(typeName)); + buf.write(' "$localKey" int REFERENCES $foreignTable($foreignKey)'); + if (relationship.cascadeOnDelete != false) + buf.write(' ON DELETE CASCADE'); + } + }); + + // Primary keys, unique + bool hasPrimary = false; + ctx.fields.forEach((f) { + var col = ctx.columnInfo[f.name]; + if (col != null) { + var name = ctx.resolveFieldName(f.name); + if (col.index == IndexType.UNIQUE) { + if (i++ > 0) buf.writeln(','); + buf.write(' UNIQUE($name('); + } else if (col.index == IndexType.PRIMARY_KEY) { + if (i++ > 0) buf.writeln(','); + hasPrimary = true; + buf.write(' PRIMARY KEY($name)'); + } + } + }); + + if (!hasPrimary) { + var idField = + ctx.fields.firstWhere((f) => f.name == 'id', orElse: () => null); + if (idField != null) { + if (i++ > 0) buf.writeln(','); + buf.write(' PRIMARY KEY(id)'); + } + } + buf.writeln(); buf.writeln(');'); } diff --git a/angel_orm_generator/lib/src/builder/postgres/postgres.dart b/angel_orm_generator/lib/src/builder/postgres/postgres.dart index c490ab05..eeba8b5d 100644 --- a/angel_orm_generator/lib/src/builder/postgres/postgres.dart +++ b/angel_orm_generator/lib/src/builder/postgres/postgres.dart @@ -2,8 +2,10 @@ import 'dart:async'; import 'package:analyzer/dart/element/element.dart'; import 'package:angel_orm/angel_orm.dart'; import 'package:build/build.dart'; +import 'package:code_builder/dart/async.dart'; import 'package:code_builder/dart/core.dart'; import 'package:code_builder/code_builder.dart'; +import 'package:inflection/inflection.dart'; import 'package:path/path.dart' as p; import 'package:recase/recase.dart'; import 'package:source_gen/src/annotation.dart'; @@ -125,7 +127,9 @@ class PostgresORMGenerator extends GeneratorForAnnotation { var m = new MethodBuilder('sort$sort', returnType: lib$core.$void); m.addPositional(parameter('key', [lib$core.String])); m.addStatement(literal(sort).asAssign(reference('_sortMode'))); - m.addStatement(reference('key').asAssign(reference('_sortKey'))); + m.addStatement((literal(ctx.prefix) + reference('key')) + .parentheses() + .asAssign(reference('_sortKey'))); clazz.addMethod(m); }); @@ -199,8 +203,31 @@ class PostgresORMGenerator extends GeneratorForAnnotation { return clazz; } + String computeSelector(PostgresBuildContext ctx) { + var buf = new StringBuffer(); + int i = 0; + + // Add all regular fields + ctx.fields.forEach((f) { + if (i++ > 0) buf.write(', '); + var name = ctx.resolveFieldName(f.name); + buf.write(ctx.prefix + "$name"); + }); + + // Add all relationship fields... + ctx.relationships.forEach((name, r) { + var relationship = ctx.populateRelationship(name); + relationship.modelTypeContext.fields.forEach((f) { + if (i++ > 0) buf.write(', '); + var name = relationship.modelTypeContext.resolveFieldName(f.name); + buf.write('${relationship.foreignTable}.$name'); + }); + }); + + return buf.toString(); + } + MethodBuilder buildToSqlMethod(PostgresBuildContext ctx) { - // TODO: Bake relationships into SQL queries var meth = new MethodBuilder('toSql', returnType: lib$core.String); meth.addPositional(parameter('prefix', [lib$core.String]).asOptional()); var buf = reference('buf'); @@ -210,11 +237,27 @@ class PostgresORMGenerator extends GeneratorForAnnotation { // Write prefix, or default to SELECT var prefix = reference('prefix'); meth.addStatement(buf.invoke('write', [ - prefix - .notEquals(literal(null)) - .ternary(prefix, literal('SELECT * FROM "${ctx.tableName}"')) + prefix.notEquals(literal(null)).ternary(prefix, + literal('SELECT ${computeSelector(ctx)} FROM "${ctx.tableName}"')) ])); + var relationsIfThen = ifThen(prefix.equals(literal(null))); + + // Apply relationships + ctx.relationships.forEach((name, r) { + var relationship = ctx.populateRelationship(name); + + // TODO: Has one, has many, belongs to many + if (relationship.type == RelationshipType.BELONGS_TO) { + var b = new StringBuffer( + ' INNER JOIN ${relationship.foreignTable} ON ${ctx.tableName}.${relationship.localKey} = ${relationship.foreignTable}.${relationship.foreignKey}'); + relationsIfThen + .addStatement(buf.invoke('write', [literal(b.toString())])); + } + }); + + meth.addStatement(relationsIfThen); + meth.addStatement(varField('whereClause', value: reference('where').invoke('toWhereClause', []))); @@ -308,32 +351,62 @@ class PostgresORMGenerator extends GeneratorForAnnotation { data[name] = rowKey; }); - ctx.relationships.forEach((name, relationship) { - var field = ctx.resolveRelationshipField(name); - var alias = ctx.resolveFieldName(name); - var idx = i++; - var rowKey = row[literal(idx)]; - data[alias] = (row.property('length') < literal(idx + 1)).ternary( - literal(null), - new TypeBuilder(new ReCase(field.type.name).pascalCase + 'Query') - .invoke('parseRow', [rowKey])); + // Invoke fromJson() + var result = reference('result'); + meth.addStatement(varField('result', + value: ctx.modelClassBuilder + .newInstance([map(data)], constructor: 'fromJson'))); + + // For each relationship, try to parse + ctx.relationships.forEach((name, r) { + int minIndex = i; + + var relationship = ctx.populateRelationship(name); + var rc = new ReCase(relationship.dartType.name); + var relationshipQuery = new TypeBuilder('${rc.pascalCase}Query'); + List relationshipRow = []; + + relationship.modelTypeContext.fields.forEach((f) { + relationshipRow.add(row[literal(i++)]); + }); + + meth.addStatement(ifThen(row.property('length') > literal(minIndex), [ + relationshipQuery.invoke( + 'parseRow', [list(relationshipRow)]).asAssign(result.property(name)) + ])); }); // Then, call a .fromJson() constructor - meth.addStatement(ctx.modelClassBuilder - .newInstance([map(data)], constructor: 'fromJson').asReturn()); + meth.addStatement(result.asReturn()); return meth; } - void _invokeStreamClosure(ExpressionBuilder future, MethodBuilder meth) { + void _invokeStreamClosure( + PostgresBuildContext ctx, ExpressionBuilder future, MethodBuilder meth) { var ctrl = reference('ctrl'); // Invoke query... var catchError = ctrl.property('addError'); - var then = new MethodBuilder.closure()..addPositional(parameter('rows')); - then.addStatement(reference('rows') - .invoke('map', [reference('parseRow')]).invoke( - 'forEach', [ctrl.property('add')])); + var then = new MethodBuilder.closure(modifier: MethodModifier.asAsync) + ..addPositional(parameter('rows')); + + var forEachClosure = + new MethodBuilder.closure(modifier: MethodModifier.asAsync); + forEachClosure.addPositional(parameter('row')); + forEachClosure.addStatement(varField('parsed', + value: reference('parseRow').call([reference('row')]))); + _applyRelationshipsToOutput( + ctx, reference('parsed'), reference('row'), forEachClosure); + forEachClosure.addStatement(reference('parsed').asReturn()); + + then.addStatement(varField('futures', + value: reference('rows').invoke('map', [forEachClosure]))); + then.addStatement(varField('output', + value: + lib$async.Future.invoke('wait', [reference('futures')]).asAwait())); + then.addStatement( + reference('output').invoke('forEach', [ctrl.property('add')])); + then.addStatement(ctrl.invoke('close', [])); meth.addStatement( future.invoke('then', [then]).invoke('catchError', [catchError])); @@ -353,7 +426,7 @@ class PostgresORMGenerator extends GeneratorForAnnotation { var future = reference('connection').invoke('query', [reference('toSql').call([])]); - _invokeStreamClosure(future, meth); + _invokeStreamClosure(ctx, future, meth); return meth; } @@ -364,16 +437,23 @@ class PostgresORMGenerator extends GeneratorForAnnotation { meth.addPositional(parameter('id', [lib$core.int])); meth.addPositional( parameter('connection', [ctx.postgreSQLConnectionBuilder])); - meth.addStatement(reference('connection').invoke('query', [ - literal('SELECT * FROM "${ctx.tableName}" WHERE "id" = @id;') - ], namedArguments: { - 'substitutionValues': map({'id': reference('id')}) - }).invoke('then', [ - new MethodBuilder.closure( - returns: - reference('parseRow').call([reference('rows').property('first')])) - ..addPositional(parameter('rows')) - ]).asReturn()); + + var query = reference('query'), + whereId = query.property('where').property('id'); + meth.addStatement( + varField('query', value: ctx.queryClassBuilder.newInstance([]))); + meth.addStatement(whereId.invoke('equals', [reference('id')])); + + // Return null on error + var catchErr = new MethodBuilder.closure(returns: literal(null)); + catchErr.addPositional(parameter('_')); + + meth.addStatement(query + .invoke('get', [reference('connection')]) + .property('first') + .invoke('catchError', [catchErr]) + .asReturn()); + return meth; } @@ -491,7 +571,7 @@ class PostgresORMGenerator extends GeneratorForAnnotation { $buf.invoke('toString', []) + literal(buf2.toString()), meth, substitutionValues); - _invokeStreamClosure(result, meth); + _invokeStreamClosure(ctx, result, meth); return meth; } @@ -514,7 +594,7 @@ class PostgresORMGenerator extends GeneratorForAnnotation { reference('toSql').call([literal('DELETE FROM "${ctx.tableName}"')]) + literal(litBuf.toString()) ]); - _invokeStreamClosure(future, meth); + _invokeStreamClosure(ctx, future, meth); return meth; } @@ -592,18 +672,71 @@ class PostgresORMGenerator extends GeneratorForAnnotation { var connection = reference('connection'); var query = literal(buf.toString()); - var result = reference('result'); + var result = reference('result'), output = reference('output'); meth.addStatement(varField('result', value: connection.invoke('query', [ query ], namedArguments: { 'substitutionValues': map(substitutionValues) }).asAwait())); - meth.addStatement( - reference('parseRow').call([result[literal(0)]]).asReturn()); + + meth.addStatement(varField('output', + value: reference('parseRow').call([result[literal(0)]]))); + + _applyRelationshipsToOutput(ctx, output, result[literal(0)], meth); + + meth.addStatement(output.asReturn()); return meth; } + void _applyRelationshipsToOutput(PostgresBuildContext ctx, + ExpressionBuilder output, ExpressionBuilder row, MethodBuilder meth) { + // Every relationship should fill itself in with a query + // TODO: Has one, has many, belongs to many + ctx.relationships.forEach((name, r) { + var relationship = ctx.populateRelationship(name); + + if (relationship.type == RelationshipType.BELONGS_TO) { + var rc = new ReCase(relationship.dartType.name); + var type = new TypeBuilder('${rc.pascalCase}Query'); + + // Resolve index within row... + bool matched = false; + int col = 0; + for (var field in ctx.fields) { + if (field is RelationshipConstraintField && + field.originalName == name) { + matched = true; + break; + } else + col++; + } + + if (!matched) + throw 'Couldn\'t resolve row index for relationship "${name}".'; + + var idAsInt = row[literal(col)]; + meth.addStatement(type + .invoke('getOne', [idAsInt, reference('connection')]) + .asAwait() + .asAssign(output.property(name))); + } + }); + } + + void _addRelationshipConstraintsNamed( + MethodBuilder m, PostgresBuildContext ctx) { + ctx.relationships.forEach((name, r) { + var relationship = ctx.populateRelationship(name); + + // TODO: Belongs to many + if (relationship.type == RelationshipType.BELONGS_TO) { + var rc = new ReCase(relationship.localKey); + m.addNamed(parameter(rc.camelCase, [lib$core.int])); + } + }); + } + MethodBuilder buildInsertModelMethod(PostgresBuildContext ctx) { var rc = new ReCase(ctx.modelClassName); var meth = new MethodBuilder('insert${rc.pascalCase}', @@ -613,12 +746,17 @@ class PostgresORMGenerator extends GeneratorForAnnotation { meth.addPositional( parameter('connection', [ctx.postgreSQLConnectionBuilder])); meth.addPositional(parameter(rc.camelCase, [ctx.modelClassBuilder])); + _addRelationshipConstraintsNamed(meth, ctx); Map args = {}; var ref = reference(rc.camelCase); ctx.fields.forEach((f) { - if (f.name != 'id') args[f.name] = ref.property(f.name); + if (f.name != 'id') { + args[f.name] = f is RelationshipConstraintField + ? reference(f.name) + : ref.property(f.name); + } }); meth.addStatement(ctx.queryClassBuilder @@ -652,7 +790,16 @@ class PostgresORMGenerator extends GeneratorForAnnotation { // return query.update(connection, ...).first; Map args = {}; ctx.fields.forEach((f) { - if (f.name != 'id') args[f.name] = ref.property(f.name); + if (f.name != 'id') { + if (f is RelationshipConstraintField) { + // Need to int.parse the related id and pass it + var relation = ref.property(f.originalName); + var relationship = ctx.populateRelationship(f.originalName); + args[f.name] = lib$core.int + .invoke('parse', [relation.property(relationship.foreignKey)]); + } else + args[f.name] = ref.property(f.name); + } }); var update = @@ -693,7 +840,8 @@ class PostgresORMGenerator extends GeneratorForAnnotation { break; case 'DateTime': queryBuilderType = new TypeBuilder('DateTimeSqlExpressionBuilder'); - args.add(literal(ctx.resolveFieldName(field.name))); + args.add(literal( + ctx.tableName + '.' + ctx.resolveFieldName(field.name))); break; } } @@ -719,9 +867,10 @@ class PostgresORMGenerator extends GeneratorForAnnotation { ctx.fields.forEach((field) { var name = ctx.resolveFieldName(field.name); var queryBuilder = reference(field.name); - var toAdd = field.type.name == 'DateTime' + var toAdd = field.type.isAssignableTo(ctx.dateTimeType) ? queryBuilder.invoke('compile', []) - : (literal('"$name" ') + queryBuilder.invoke('compile', [])); + : (literal('${ctx.tableName}.$name ') + + queryBuilder.invoke('compile', [])); toWhereClause.addStatement(ifThen(queryBuilder.property('hasValue'), [ expressions.invoke('add', [toAdd]) diff --git a/angel_orm_generator/lib/src/builder/postgres/postgres_build_context.dart b/angel_orm_generator/lib/src/builder/postgres/postgres_build_context.dart index 289e05e8..ab3e8c79 100644 --- a/angel_orm_generator/lib/src/builder/postgres/postgres_build_context.dart +++ b/angel_orm_generator/lib/src/builder/postgres/postgres_build_context.dart @@ -1,3 +1,4 @@ +import 'package:analyzer/dart/constant/value.dart'; import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/type.dart'; import 'package:analyzer/src/generated/resolver.dart'; @@ -5,6 +6,10 @@ import 'package:angel_orm/angel_orm.dart'; import 'package:angel_serialize_generator/context.dart'; import 'package:build/build.dart'; import 'package:code_builder/code_builder.dart'; +import 'package:inflection/inflection.dart'; +import 'package:recase/recase.dart'; +import 'package:source_gen/source_gen.dart'; +import 'build_context.dart'; class PostgresBuildContext extends BuildContext { DartType _dateTimeTypeCache; @@ -14,9 +19,12 @@ class PostgresBuildContext extends BuildContext { _queryClassBuilder, _whereClassBuilder, _postgresqlConnectionBuilder; + String _prefix; + final Map _populatedRelationships = {}; final Map columnInfo = {}; final Map indices = {}; final Map relationships = {}; + final bool autoSnakeCaseNames, autoIdAndDateFields; final String tableName; final ORM ormAnnotation; final BuildContext raw; @@ -26,7 +34,7 @@ class PostgresBuildContext extends BuildContext { PostgresBuildContext( this.raw, this.ormAnnotation, this.resolver, this.buildStep, - {this.tableName}) + {this.tableName, this.autoSnakeCaseNames, this.autoIdAndDateFields}) : super(raw.annotation, originalClassName: raw.originalClassName, sourceFilename: raw.sourceFilename); @@ -45,6 +53,14 @@ class PostgresBuildContext extends BuildContext { TypeBuilder get postgreSQLConnectionBuilder => _postgresqlConnectionBuilder ??= new TypeBuilder('PostgreSQLConnection'); + String get prefix { + if (_prefix != null) return _prefix; + if (relationships.isEmpty) + return _prefix = ''; + else + return _prefix = tableName + '.'; + } + Map get aliases => raw.aliases; Map get shimmed => raw.shimmed; @@ -71,4 +87,110 @@ class PostgresBuildContext extends BuildContext { FieldElement resolveRelationshipField(String name) => relationshipFields.firstWhere((f) => f.name == name, orElse: () => null); + + PopulatedRelationship populateRelationship(String name) { + return _populatedRelationships.putIfAbsent(name, () { + // TODO: Belongs to many + var f = raw.fields.firstWhere((f) => f.name == name); + var relationship = relationships[name]; + var typeName = + f.type.name.startsWith('_') ? f.type.name.substring(1) : f.type.name; + var rc = new ReCase(typeName); + + if (relationship.type == RelationshipType.HAS_ONE || + relationship.type == RelationshipType.HAS_MANY) { + var foreignKey = relationship.localKey ?? + (autoSnakeCaseNames != false + ? '${rc.snakeCase}_id' + : '${typeName}Id'); + var localKey = relationship.foreignKey ?? 'id'; + var foreignTable = relationship.foreignTable ?? + (autoSnakeCaseNames != false + ? pluralize(rc.snakeCase) + : pluralize(typeName)); + return new PopulatedRelationship(relationship.type, f.type, buildStep, + resolver, autoSnakeCaseNames, autoIdAndDateFields, + localKey: localKey, + foreignKey: foreignKey, + foreignTable: foreignTable, + cascadeOnDelete: relationship.cascadeOnDelete); + } else if (relationship.type == RelationshipType.BELONGS_TO) { + var localKey = relationship.localKey ?? + (autoSnakeCaseNames != false + ? '${rc.snakeCase}_id' + : '${typeName}Id'); + var foreignKey = relationship.foreignKey ?? 'id'; + var foreignTable = relationship.foreignTable ?? + (autoSnakeCaseNames != false + ? pluralize(rc.snakeCase) + : pluralize(typeName)); + return new PopulatedRelationship(relationship.type, f.type, buildStep, + resolver, autoSnakeCaseNames, autoIdAndDateFields, + localKey: localKey, + foreignKey: foreignKey, + foreignTable: foreignTable, + cascadeOnDelete: relationship.cascadeOnDelete); + } else + throw new UnsupportedError( + 'Invalid relationship type: ${relationship.type}'); + }); + } +} + +class PopulatedRelationship extends Relationship { + DartType _modelType; + PostgresBuildContext _modelTypeContext; + DartObject _modelTypeORM; + final DartType dartType; + final BuildStep buildStep; + final Resolver resolver; + final bool autoSnakeCaseNames, autoIdAndDateFields; + + PopulatedRelationship(int type, this.dartType, this.buildStep, this.resolver, + this.autoSnakeCaseNames, this.autoIdAndDateFields, + {String localKey, + String foreignKey, + String foreignTable, + bool cascadeOnDelete}) + : super(type, + localKey: localKey, + foreignKey: foreignKey, + foreignTable: foreignTable, + cascadeOnDelete: cascadeOnDelete); + + DartType get modelType { + if (_modelType != null) return _modelType; + DartType searchType = dartType; + var ormChecker = new TypeChecker.fromRuntime(ORM); + + while (searchType != null) { + var classElement = searchType.element as ClassElement; + var ormAnnotation = ormChecker.firstAnnotationOf(classElement); + + if (ormAnnotation != null) { + _modelTypeORM = ormAnnotation; + return _modelType = searchType; + } else { + // If we didn't find an @ORM(), then refer to the parent type. + searchType = classElement.supertype; + } + } + + throw new StateError( + 'Neither ${dartType.name} nor its parent types are annotated with an @ORM() annotation. It is impossible to compute this relationship.'); + } + + PostgresBuildContext get modelTypeContext { + if (_modelTypeContext != null) return _modelTypeContext; + var reader = new ConstantReader(_modelTypeORM); + if (reader.isNull) + reader = null; + else + reader = reader.read('tableName'); + var orm = reader == null + ? new ORM() + : new ORM(reader.isString ? reader.stringValue : null); + return _modelTypeContext = buildContext(modelType.element, orm, buildStep, + resolver, autoSnakeCaseNames, autoIdAndDateFields); + } } diff --git a/angel_orm_generator/pubspec.yaml b/angel_orm_generator/pubspec.yaml index 0bb44716..c947205b 100644 --- a/angel_orm_generator/pubspec.yaml +++ b/angel_orm_generator/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_orm_generator -version: 1.0.0-alpha+1 +version: 1.0.0-alpha+2 description: Code generators for Angel's ORM. author: Tobe O homepage: https://github.com/angel-dart/orm diff --git a/angel_orm_generator/test/book_test.dart b/angel_orm_generator/test/book_test.dart new file mode 100644 index 00000000..9bddb05c --- /dev/null +++ b/angel_orm_generator/test/book_test.dart @@ -0,0 +1,135 @@ +import 'package:angel_orm/angel_orm.dart'; +import 'package:postgres/postgres.dart'; +import 'package:test/test.dart'; +import 'models/author.dart'; +import 'models/author.orm.g.dart'; +import 'models/book.dart'; +import 'models/book.orm.g.dart'; +import 'common.dart'; + +main() { + PostgreSQLConnection connection; + Author rowling; + Book deathlyHallows; + + setUp(() async { + connection = await connectToPostgres(); + + // Insert an author + rowling = await AuthorQuery.insert(connection, name: 'J.K. Rowling'); + + // And a book + deathlyHallows = await BookQuery.insert(connection, + authorId: int.parse(rowling.id), name: 'Deathly Hallows'); + }); + + tearDown(() => connection.close()); + + group('selects', () + { + test('select all', () async { + var query = new BookQuery(); + var books = await query.get(connection).toList(); + expect(books, hasLength(1)); + + var book = books.first; + print(book.toJson()); + expect(book.id, deathlyHallows.id); + expect(book.name, deathlyHallows.name); + + var author = book.author as Author; + print(author.toJson()); + expect(author.id, rowling.id); + expect(author.name, rowling.name); + }); + + test('select one', () async { + var query = new BookQuery(); + query.where.id.equals(int.parse(deathlyHallows.id)); + print(query.toSql()); + + var book = await BookQuery.getOne( + int.parse(deathlyHallows.id), connection); + print(book.toJson()); + expect(book.id, deathlyHallows.id); + expect(book.name, deathlyHallows.name); + + var author = book.author as Author; + print(author.toJson()); + expect(author.id, rowling.id); + expect(author.name, rowling.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()); + + var books = await query.get(connection).toList(); + expect(books, hasLength(1)); + + var book = books.first; + print(book.toJson()); + expect(book.id, deathlyHallows.id); + expect(book.name, deathlyHallows.name); + + var author = book.author as Author; + print(author.toJson()); + expect(author.id, rowling.id); + expect(author.name, rowling.name); + }); + + test('union', () async { + var query1 = new BookQuery() + ..where.name.like('Deathly%'); + var query2 = new BookQuery() + ..where.authorId.equals(-1); + var query3 = new BookQuery() + ..where.name.isIn(['Goblet of Fire', 'Order of the Phoenix']); + query1 + ..union(query2) + ..unionAll(query3); + print(query1.toSql()); + + var books = await query1.get(connection).toList(); + expect(books, hasLength(1)); + + var book = books.first; + print(book.toJson()); + expect(book.id, deathlyHallows.id); + expect(book.name, deathlyHallows.name); + + var author = book.author as Author; + print(author.toJson()); + expect(author.id, rowling.id); + expect(author.name, rowling.name); + }); + }); + + test('insert sets relationship', () { + expect(deathlyHallows.author, isNotNull); + expect((deathlyHallows.author as 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(); + expect(books, hasLength(1)); + + var book = books.first; + expect(book.id, deathlyHallows.id); + expect(book.author, isNotNull); + expect((book.author as Author).name, rowling.name); + }); + + test('update book', () async { + var cloned = deathlyHallows.clone()..name = 'Sorcerer\'s Stone'; + var book = await BookQuery.updateBook(connection, cloned); + print(book.toJson()); + expect(book.name, cloned.name); + expect(book.author, isNotNull); + expect((book.author as Author).name, rowling.name); + }); +} diff --git a/angel_orm_generator/test/car_test.dart b/angel_orm_generator/test/car_test.dart index 7f3d0a98..9381c477 100644 --- a/angel_orm_generator/test/car_test.dart +++ b/angel_orm_generator/test/car_test.dart @@ -16,7 +16,7 @@ main() { var whereClause = query.where.toWhereClause(); print('Where clause: $whereClause'); expect(whereClause, - 'WHERE "family_friendly" = TRUE AND "recalled_at" <= \'2000-01-01\''); + 'WHERE cars.family_friendly = TRUE AND cars.recalled_at <= \'2000-01-01\''); }); test('parseRow', () { diff --git a/angel_orm_generator/test/common.dart b/angel_orm_generator/test/common.dart index ab0e2eae..ac4417c1 100644 --- a/angel_orm_generator/test/common.dart +++ b/angel_orm_generator/test/common.dart @@ -10,6 +10,10 @@ Future connectToPostgres() async { var query = await new File('test/models/car.up.g.sql').readAsString(); await conn.execute(query); + query = await new File('test/models/author.up.g.sql').readAsString(); + await conn.execute(query); + query = await new File('test/models/book.up.g.sql').readAsString(); + await conn.execute(query); return conn; } diff --git a/angel_orm_generator/test/models/author.orm.g.dart b/angel_orm_generator/test/models/author.orm.g.dart index 02f38433..97544260 100644 --- a/angel_orm_generator/test/models/author.orm.g.dart +++ b/angel_orm_generator/test/models/author.orm.g.dart @@ -35,12 +35,12 @@ class AuthorQuery { void sortDescending(String key) { _sortMode = 'Descending'; - _sortKey = key; + _sortKey = ('' + key); } void sortAscending(String key) { _sortMode = 'Ascending'; - _sortKey = key; + _sortKey = ('' + key); } void or(AuthorQueryWhere selector) { @@ -49,7 +49,10 @@ class AuthorQuery { String toSql([String prefix]) { var buf = new StringBuffer(); - buf.write(prefix != null ? prefix : 'SELECT * FROM "authors"'); + buf.write(prefix != null + ? prefix + : 'SELECT id, name, created_at, updated_at FROM "authors"'); + if (prefix == null) {} var whereClause = where.toWhereClause(); if (whereClause != null) { buf.write(' ' + whereClause); @@ -88,26 +91,33 @@ class AuthorQuery { } static Author parseRow(List row) { - return new Author.fromJson({ + var result = new Author.fromJson({ 'id': row[0].toString(), 'name': row[1], 'created_at': row[2], 'updated_at': row[3] }); + return result; } Stream get(PostgreSQLConnection connection) { StreamController ctrl = new StreamController(); - connection.query(toSql()).then((rows) { - rows.map(parseRow).forEach(ctrl.add); + connection.query(toSql()).then((rows) async { + var futures = rows.map((row) async { + var parsed = parseRow(row); + return parsed; + }); + var output = await Future.wait(futures); + output.forEach(ctrl.add); ctrl.close(); }).catchError(ctrl.addError); return ctrl.stream; } static Future getOne(int id, PostgreSQLConnection connection) { - return connection.query('SELECT * FROM "authors" WHERE "id" = @id;', - substitutionValues: {'id': id}).then((rows) => parseRow(rows.first)); + var query = new AuthorQuery(); + query.where.id.equals(id); + return query.get(connection).first.catchError((_) => null); } Stream update(PostgreSQLConnection connection, @@ -128,8 +138,13 @@ class AuthorQuery { 'name': name, 'createdAt': createdAt != null ? createdAt : __ormNow__, 'updatedAt': updatedAt != null ? updatedAt : __ormNow__ - }).then((rows) { - rows.map(parseRow).forEach(ctrl.add); + }).then((rows) async { + var futures = rows.map((row) async { + var parsed = parseRow(row); + return parsed; + }); + var output = await Future.wait(futures); + output.forEach(ctrl.add); ctrl.close(); }).catchError(ctrl.addError); return ctrl.stream; @@ -140,8 +155,13 @@ class AuthorQuery { connection .query(toSql('DELETE FROM "authors"') + ' RETURNING "id", "name", "created_at", "updated_at";') - .then((rows) { - rows.map(parseRow).forEach(ctrl.add); + .then((rows) async { + var futures = rows.map((row) async { + var parsed = parseRow(row); + return parsed; + }); + var output = await Future.wait(futures); + output.forEach(ctrl.add); ctrl.close(); }).catchError(ctrl.addError); return ctrl.stream; @@ -165,7 +185,8 @@ class AuthorQuery { 'createdAt': createdAt != null ? createdAt : __ormNow__, 'updatedAt': updatedAt != null ? updatedAt : __ormNow__ }); - return parseRow(result[0]); + var output = parseRow(result[0]); + return output; } static Future insertAuthor( @@ -199,18 +220,18 @@ class AuthorQueryWhere { final StringSqlExpressionBuilder name = new StringSqlExpressionBuilder(); final DateTimeSqlExpressionBuilder createdAt = - new DateTimeSqlExpressionBuilder('created_at'); + new DateTimeSqlExpressionBuilder('authors.created_at'); final DateTimeSqlExpressionBuilder updatedAt = - new DateTimeSqlExpressionBuilder('updated_at'); + new DateTimeSqlExpressionBuilder('authors.updated_at'); String toWhereClause({bool keyword}) { final List expressions = []; if (id.hasValue) { - expressions.add('"id" ' + id.compile()); + expressions.add('authors.id ' + id.compile()); } if (name.hasValue) { - expressions.add('"name" ' + name.compile()); + expressions.add('authors.name ' + name.compile()); } if (createdAt.hasValue) { expressions.add(createdAt.compile()); diff --git a/angel_orm_generator/test/models/author.up.g.sql b/angel_orm_generator/test/models/author.up.g.sql index d893e0ce..802e83a0 100644 --- a/angel_orm_generator/test/models/author.up.g.sql +++ b/angel_orm_generator/test/models/author.up.g.sql @@ -2,5 +2,6 @@ CREATE TEMPORARY TABLE "authors" ( "id" serial, "name" varchar, "created_at" timestamp, - "updated_at" timestamp + "updated_at" timestamp, + PRIMARY KEY(id) ); diff --git a/angel_orm_generator/test/models/book.dart b/angel_orm_generator/test/models/book.dart index 392438da..9a153936 100644 --- a/angel_orm_generator/test/models/book.dart +++ b/angel_orm_generator/test/models/book.dart @@ -1,4 +1,4 @@ -library angel_orm.test.models.author; +library angel_orm.test.models.book; import 'package:angel_framework/common.dart'; import 'package:angel_orm/angel_orm.dart'; diff --git a/angel_orm_generator/test/models/book.g.dart b/angel_orm_generator/test/models/book.g.dart index 7f2c01df..9360456c 100644 --- a/angel_orm_generator/test/models/book.g.dart +++ b/angel_orm_generator/test/models/book.g.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of angel_orm.test.models.author; +part of angel_orm.test.models.book; // ************************************************************************** // Generator: JsonModelGenerator diff --git a/angel_orm_generator/test/models/book.orm.g.dart b/angel_orm_generator/test/models/book.orm.g.dart index a3dde4ba..d1339a39 100644 --- a/angel_orm_generator/test/models/book.orm.g.dart +++ b/angel_orm_generator/test/models/book.orm.g.dart @@ -36,12 +36,12 @@ class BookQuery { void sortDescending(String key) { _sortMode = 'Descending'; - _sortKey = key; + _sortKey = ('books.' + key); } void sortAscending(String key) { _sortMode = 'Ascending'; - _sortKey = key; + _sortKey = ('books.' + key); } void or(BookQueryWhere selector) { @@ -50,7 +50,12 @@ class BookQuery { String toSql([String prefix]) { var buf = new StringBuffer(); - buf.write(prefix != null ? prefix : 'SELECT * FROM "books"'); + buf.write(prefix != null + ? prefix + : 'SELECT books.id, books.name, books.created_at, books.updated_at, books.author_id, authors.id, authors.name, authors.created_at, authors.updated_at FROM "books"'); + if (prefix == null) { + buf.write(' INNER JOIN authors ON books.author_id = authors.id'); + } var whereClause = where.toWhereClause(); if (whereClause != null) { buf.write(' ' + whereClause); @@ -89,33 +94,44 @@ class BookQuery { } static Book parseRow(List row) { - return new Book.fromJson({ + var result = new Book.fromJson({ 'id': row[0].toString(), 'name': row[1], 'created_at': row[2], 'updated_at': row[3], - 'author': row.length < 5 ? null : AuthorQuery.parseRow(row[4]) + 'author_id': row[4] }); + if (row.length > 5) { + result.author = AuthorQuery.parseRow([row[5], row[6], row[7], row[8]]); + } + return result; } Stream get(PostgreSQLConnection connection) { StreamController ctrl = new StreamController(); - connection.query(toSql()).then((rows) { - rows.map(parseRow).forEach(ctrl.add); + connection.query(toSql()).then((rows) async { + var futures = rows.map((row) async { + var parsed = parseRow(row); + parsed.author = await AuthorQuery.getOne(row[4], connection); + return parsed; + }); + var output = await Future.wait(futures); + output.forEach(ctrl.add); ctrl.close(); }).catchError(ctrl.addError); return ctrl.stream; } static Future getOne(int id, PostgreSQLConnection connection) { - return connection.query('SELECT * FROM "books" WHERE "id" = @id;', - substitutionValues: {'id': id}).then((rows) => parseRow(rows.first)); + var query = new BookQuery(); + query.where.id.equals(id); + return query.get(connection).first.catchError((_) => null); } Stream update(PostgreSQLConnection connection, - {String name, DateTime createdAt, DateTime updatedAt}) { + {String name, DateTime createdAt, DateTime updatedAt, int authorId}) { var buf = new StringBuffer( - 'UPDATE "books" SET ("name", "created_at", "updated_at") = (@name, @createdAt, @updatedAt) '); + 'UPDATE "books" SET ("name", "created_at", "updated_at", "author_id") = (@name, @createdAt, @updatedAt, @authorId) '); var whereClause = where.toWhereClause(); if (whereClause == null) { buf.write('WHERE "id" = @id'); @@ -125,13 +141,21 @@ class BookQuery { var __ormNow__ = new DateTime.now(); var ctrl = new StreamController(); connection.query( - buf.toString() + ' RETURNING "id", "name", "created_at", "updated_at";', + buf.toString() + + ' RETURNING "id", "name", "created_at", "updated_at", "author_id";', substitutionValues: { 'name': name, 'createdAt': createdAt != null ? createdAt : __ormNow__, - 'updatedAt': updatedAt != null ? updatedAt : __ormNow__ - }).then((rows) { - rows.map(parseRow).forEach(ctrl.add); + 'updatedAt': updatedAt != null ? updatedAt : __ormNow__, + 'authorId': authorId + }).then((rows) async { + var futures = rows.map((row) async { + var parsed = parseRow(row); + parsed.author = await AuthorQuery.getOne(row[4], connection); + return parsed; + }); + var output = await Future.wait(futures); + output.forEach(ctrl.add); ctrl.close(); }).catchError(ctrl.addError); return ctrl.stream; @@ -141,9 +165,15 @@ class BookQuery { StreamController ctrl = new StreamController(); connection .query(toSql('DELETE FROM "books"') + - ' RETURNING "id", "name", "created_at", "updated_at";') - .then((rows) { - rows.map(parseRow).forEach(ctrl.add); + ' RETURNING "id", "name", "created_at", "updated_at", "author_id";') + .then((rows) async { + var futures = rows.map((row) async { + var parsed = parseRow(row); + parsed.author = await AuthorQuery.getOne(row[4], connection); + return parsed; + }); + var output = await Future.wait(futures); + output.forEach(ctrl.add); ctrl.close(); }).catchError(ctrl.addError); return ctrl.stream; @@ -151,27 +181,37 @@ class BookQuery { static Future deleteOne(int id, PostgreSQLConnection connection) async { var result = await connection.query( - 'DELETE FROM "books" WHERE id = @id RETURNING "id", "name", "created_at", "updated_at";', + 'DELETE FROM "books" WHERE id = @id RETURNING "id", "name", "created_at", "updated_at", "author_id";', substitutionValues: {'id': id}); return parseRow(result[0]); } static Future insert(PostgreSQLConnection connection, - {String name, DateTime createdAt, DateTime updatedAt}) async { + {String name, + DateTime createdAt, + DateTime updatedAt, + int authorId}) async { var __ormNow__ = new DateTime.now(); var result = await connection.query( - 'INSERT INTO "books" ("name", "created_at", "updated_at") VALUES (@name, @createdAt, @updatedAt) RETURNING "id", "name", "created_at", "updated_at";', + 'INSERT INTO "books" ("name", "created_at", "updated_at", "author_id") VALUES (@name, @createdAt, @updatedAt, @authorId) RETURNING "id", "name", "created_at", "updated_at", "author_id";', substitutionValues: { 'name': name, 'createdAt': createdAt != null ? createdAt : __ormNow__, - 'updatedAt': updatedAt != null ? updatedAt : __ormNow__ + 'updatedAt': updatedAt != null ? updatedAt : __ormNow__, + 'authorId': authorId }); - return parseRow(result[0]); + var output = parseRow(result[0]); + output.author = await AuthorQuery.getOne(result[0][4], connection); + return output; } - static Future insertBook(PostgreSQLConnection connection, Book book) { + static Future insertBook(PostgreSQLConnection connection, Book book, + {int authorId}) { return BookQuery.insert(connection, - name: book.name, createdAt: book.createdAt, updatedAt: book.updatedAt); + name: book.name, + createdAt: book.createdAt, + updatedAt: book.updatedAt, + authorId: authorId); } static Future updateBook(PostgreSQLConnection connection, Book book) { @@ -181,7 +221,8 @@ class BookQuery { .update(connection, name: book.name, createdAt: book.createdAt, - updatedAt: book.updatedAt) + updatedAt: book.updatedAt, + authorId: int.parse(book.author.id)) .first; } @@ -196,18 +237,21 @@ class BookQueryWhere { final StringSqlExpressionBuilder name = new StringSqlExpressionBuilder(); final DateTimeSqlExpressionBuilder createdAt = - new DateTimeSqlExpressionBuilder('created_at'); + new DateTimeSqlExpressionBuilder('books.created_at'); final DateTimeSqlExpressionBuilder updatedAt = - new DateTimeSqlExpressionBuilder('updated_at'); + new DateTimeSqlExpressionBuilder('books.updated_at'); + + final NumericSqlExpressionBuilder authorId = + new NumericSqlExpressionBuilder(); String toWhereClause({bool keyword}) { final List expressions = []; if (id.hasValue) { - expressions.add('"id" ' + id.compile()); + expressions.add('books.id ' + id.compile()); } if (name.hasValue) { - expressions.add('"name" ' + name.compile()); + expressions.add('books.name ' + name.compile()); } if (createdAt.hasValue) { expressions.add(createdAt.compile()); @@ -215,6 +259,9 @@ class BookQueryWhere { if (updatedAt.hasValue) { expressions.add(updatedAt.compile()); } + if (authorId.hasValue) { + expressions.add('books.author_id ' + authorId.compile()); + } return expressions.isEmpty ? null : ((keyword != false ? 'WHERE ' : '') + expressions.join(' AND ')); diff --git a/angel_orm_generator/test/models/book.up.g.sql b/angel_orm_generator/test/models/book.up.g.sql index 26689827..465e1f07 100644 --- a/angel_orm_generator/test/models/book.up.g.sql +++ b/angel_orm_generator/test/models/book.up.g.sql @@ -2,5 +2,7 @@ CREATE TEMPORARY TABLE "books" ( "id" serial, "name" varchar, "created_at" timestamp, - "updated_at" timestamp + "updated_at" timestamp, + "author_id" int REFERENCES authors(id) ON DELETE CASCADE, + PRIMARY KEY(id) ); diff --git a/angel_orm_generator/test/models/car.orm.g.dart b/angel_orm_generator/test/models/car.orm.g.dart index ab2b5422..df03bed7 100644 --- a/angel_orm_generator/test/models/car.orm.g.dart +++ b/angel_orm_generator/test/models/car.orm.g.dart @@ -35,12 +35,12 @@ class CarQuery { void sortDescending(String key) { _sortMode = 'Descending'; - _sortKey = key; + _sortKey = ('' + key); } void sortAscending(String key) { _sortMode = 'Ascending'; - _sortKey = key; + _sortKey = ('' + key); } void or(CarQueryWhere selector) { @@ -49,7 +49,10 @@ class CarQuery { String toSql([String prefix]) { var buf = new StringBuffer(); - buf.write(prefix != null ? prefix : 'SELECT * FROM "cars"'); + buf.write(prefix != null + ? prefix + : 'SELECT id, make, description, family_friendly, recalled_at, created_at, updated_at FROM "cars"'); + if (prefix == null) {} var whereClause = where.toWhereClause(); if (whereClause != null) { buf.write(' ' + whereClause); @@ -88,7 +91,7 @@ class CarQuery { } static Car parseRow(List row) { - return new Car.fromJson({ + var result = new Car.fromJson({ 'id': row[0].toString(), 'make': row[1], 'description': row[2], @@ -97,20 +100,27 @@ class CarQuery { 'created_at': row[5], 'updated_at': row[6] }); + return result; } Stream get(PostgreSQLConnection connection) { StreamController ctrl = new StreamController(); - connection.query(toSql()).then((rows) { - rows.map(parseRow).forEach(ctrl.add); + connection.query(toSql()).then((rows) async { + var futures = rows.map((row) async { + var parsed = parseRow(row); + return parsed; + }); + var output = await Future.wait(futures); + output.forEach(ctrl.add); ctrl.close(); }).catchError(ctrl.addError); return ctrl.stream; } static Future getOne(int id, PostgreSQLConnection connection) { - return connection.query('SELECT * FROM "cars" WHERE "id" = @id;', - substitutionValues: {'id': id}).then((rows) => parseRow(rows.first)); + var query = new CarQuery(); + query.where.id.equals(id); + return query.get(connection).first.catchError((_) => null); } Stream update(PostgreSQLConnection connection, @@ -140,8 +150,13 @@ class CarQuery { 'recalledAt': recalledAt, 'createdAt': createdAt != null ? createdAt : __ormNow__, 'updatedAt': updatedAt != null ? updatedAt : __ormNow__ - }).then((rows) { - rows.map(parseRow).forEach(ctrl.add); + }).then((rows) async { + var futures = rows.map((row) async { + var parsed = parseRow(row); + return parsed; + }); + var output = await Future.wait(futures); + output.forEach(ctrl.add); ctrl.close(); }).catchError(ctrl.addError); return ctrl.stream; @@ -152,8 +167,13 @@ class CarQuery { connection .query(toSql('DELETE FROM "cars"') + ' RETURNING "id", "make", "description", "family_friendly", "recalled_at", "created_at", "updated_at";') - .then((rows) { - rows.map(parseRow).forEach(ctrl.add); + .then((rows) async { + var futures = rows.map((row) async { + var parsed = parseRow(row); + return parsed; + }); + var output = await Future.wait(futures); + output.forEach(ctrl.add); ctrl.close(); }).catchError(ctrl.addError); return ctrl.stream; @@ -184,7 +204,8 @@ class CarQuery { 'createdAt': createdAt != null ? createdAt : __ormNow__, 'updatedAt': updatedAt != null ? updatedAt : __ormNow__ }); - return parseRow(result[0]); + var output = parseRow(result[0]); + return output; } static Future insertCar(PostgreSQLConnection connection, Car car) { @@ -228,27 +249,27 @@ class CarQueryWhere { new BooleanSqlExpressionBuilder(); final DateTimeSqlExpressionBuilder recalledAt = - new DateTimeSqlExpressionBuilder('recalled_at'); + new DateTimeSqlExpressionBuilder('cars.recalled_at'); final DateTimeSqlExpressionBuilder createdAt = - new DateTimeSqlExpressionBuilder('created_at'); + new DateTimeSqlExpressionBuilder('cars.created_at'); final DateTimeSqlExpressionBuilder updatedAt = - new DateTimeSqlExpressionBuilder('updated_at'); + new DateTimeSqlExpressionBuilder('cars.updated_at'); String toWhereClause({bool keyword}) { final List expressions = []; if (id.hasValue) { - expressions.add('"id" ' + id.compile()); + expressions.add('cars.id ' + id.compile()); } if (make.hasValue) { - expressions.add('"make" ' + make.compile()); + expressions.add('cars.make ' + make.compile()); } if (description.hasValue) { - expressions.add('"description" ' + description.compile()); + expressions.add('cars.description ' + description.compile()); } if (familyFriendly.hasValue) { - expressions.add('"family_friendly" ' + familyFriendly.compile()); + expressions.add('cars.family_friendly ' + familyFriendly.compile()); } if (recalledAt.hasValue) { expressions.add(recalledAt.compile()); diff --git a/angel_orm_generator/test/models/car.up.g.sql b/angel_orm_generator/test/models/car.up.g.sql index bbf81d26..4cfacf16 100644 --- a/angel_orm_generator/test/models/car.up.g.sql +++ b/angel_orm_generator/test/models/car.up.g.sql @@ -5,5 +5,6 @@ CREATE TEMPORARY TABLE "cars" ( "family_friendly" boolean, "recalled_at" timestamp, "created_at" timestamp, - "updated_at" timestamp + "updated_at" timestamp, + PRIMARY KEY(id) ); diff --git a/angel_orm_generator/tool/phases.dart b/angel_orm_generator/tool/phases.dart index 78c2dd59..40103de4 100644 --- a/angel_orm_generator/tool/phases.dart +++ b/angel_orm_generator/tool/phases.dart @@ -3,16 +3,28 @@ import 'package:source_gen/source_gen.dart'; import 'package:angel_orm_generator/angel_orm_generator.dart'; import 'package:angel_serialize_generator/angel_serialize_generator.dart'; -final InputSet MODELS = +final InputSet ALL_MODELS = new InputSet('angel_orm_generator', const ['test/models/*.dart']); +final InputSet STANDALONE_MODELS = new InputSet('angel_orm_generator', + const ['test/models/car.dart', 'test/models/author.dart']); +final InputSet DEPENDENT_MODELS = + new InputSet('angel_orm_generator', const ['test/models/book.dart']); final PhaseGroup PHASES = new PhaseGroup() ..addPhase(new Phase() - ..addAction(new GeneratorBuilder([const JsonModelGenerator()]), MODELS)) + ..addAction( + new GeneratorBuilder([const JsonModelGenerator()]), STANDALONE_MODELS) + ..addAction( + new GeneratorBuilder([const JsonModelGenerator()]), DEPENDENT_MODELS)) ..addPhase(new Phase() ..addAction( new GeneratorBuilder([new PostgresORMGenerator()], isStandalone: true, generatedExtension: '.orm.g.dart'), - MODELS)) + STANDALONE_MODELS)) ..addPhase(new Phase() - ..addAction(new SQLMigrationGenerator(temporary: true), MODELS)); + ..addAction( + new GeneratorBuilder([new PostgresORMGenerator()], + isStandalone: true, generatedExtension: '.orm.g.dart'), + DEPENDENT_MODELS)) + ..addPhase(new Phase() + ..addAction(new SQLMigrationGenerator(temporary: true), ALL_MODELS));