orm 2.1.0-beta.2

This commit is contained in:
Tobe O 2019-10-12 20:47:00 -04:00
parent 8222230c8a
commit d25e84637e
13 changed files with 688 additions and 10 deletions

View file

@ -1,3 +1,7 @@
# 2.1.0-beta.2
* Add `expressions` to `Query`, to support custom SQL expressions that are
read as normal fields.
# 2.1.0-beta.1 # 2.1.0-beta.1
* Calls to `leftJoin`, etc. alias all fields in a child query, to prevent * Calls to `leftJoin`, etc. alias all fields in a child query, to prevent
`ambiguous column a0.id` errors. `ambiguous column a0.id` errors.

View file

@ -17,6 +17,10 @@ abstract class Query<T, Where extends QueryWhere> extends QueryBase<T> {
// the parent's context. // the parent's context.
final Query parent; final Query parent;
/// A map of field names to explicit SQL expressions. The expressions will be aliased
/// to the given names.
final Map<String, String> expressions = {};
String _crossJoin, _groupBy; String _crossJoin, _groupBy;
int _limit, _offset; int _limit, _offset;
@ -36,7 +40,13 @@ abstract class Query<T, Where extends QueryWhere> extends QueryBase<T> {
QueryValues get values; QueryValues get values;
/// Preprends the [tableName] to the [String], [s]. /// Preprends the [tableName] to the [String], [s].
String adornWithTableName(String s) => '$tableName.$s'; String adornWithTableName(String s) {
if (expressions.containsKey(s)) {
return '(${expressions[s]} AS $s)';
} else {
return '$tableName.$s';
}
}
/// Returns a unique version of [name], which will not produce a collision within /// Returns a unique version of [name], which will not produce a collision within
/// the context of this [query]. /// the context of this [query].
@ -233,6 +243,10 @@ abstract class Query<T, Where extends QueryWhere> extends QueryBase<T> {
} else { } else {
f = List<String>.from(fields.map((s) { f = List<String>.from(fields.map((s) {
var ss = includeTableName ? '$tableName.$s' : s; var ss = includeTableName ? '$tableName.$s' : s;
if (expressions.containsKey(s)) {
// ss = '(' + expressions[s] + ')';
ss = expressions[s];
}
var cast = casts[s]; var cast = casts[s];
if (cast != null) ss = 'CAST ($ss AS $cast)'; if (cast != null) ss = 'CAST ($ss AS $cast)';
if (aliases.containsKey(s)) { if (aliases.containsKey(s)) {
@ -241,6 +255,15 @@ abstract class Query<T, Where extends QueryWhere> extends QueryBase<T> {
} else { } else {
ss = '$ss AS ${aliases[s]}'; ss = '$ss AS ${aliases[s]}';
} }
if (expressions.containsKey(s)) {
ss = '($ss)';
}
} else if (expressions.containsKey(s)) {
if (cast != null) {
ss = '(($ss) AS $s)';
} else {
ss = '($ss AS $s)';
}
} }
return ss; return ss;
})); }));

View file

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

View file

@ -82,6 +82,9 @@ class MigrationGenerator extends GeneratorForAnnotation<Orm> {
List<String> dup = []; List<String> dup = [];
ctx.columns.forEach((name, col) { ctx.columns.forEach((name, col) {
// Skip custom-expression columns.
if (col.hasExpression) return;
var key = ctx.buildContext.resolveFieldName(name); var key = ctx.buildContext.resolveFieldName(name);
if (dup.contains(key)) { if (dup.contains(key)) {

View file

@ -291,6 +291,17 @@ Future<OrmBuildContext> buildOrmContext(
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}".'; throw 'Cannot infer SQL column type for field "${ctx.buildContext.originalClassName}.${field.name}" with type "${field.type.displayName}".';
} }
// Expressions...
column = Column(
isNullable: column.isNullable,
length: column.length,
type: column.type,
indexType: column.indexType,
expression:
ConstantReader(columnAnnotation).peek('expression')?.stringValue,
);
ctx.columns[field.name] = column; ctx.columns[field.name] = column;
if (!ctx.effectiveFields.any((f) => f.name == field.name)) { if (!ctx.effectiveFields.any((f) => f.name == field.name)) {
@ -372,6 +383,14 @@ class OrmBuildContext {
final Map<String, RelationshipReader> relations = {}; final Map<String, RelationshipReader> relations = {};
OrmBuildContext(this.buildContext, this.ormAnnotation, this.tableName); OrmBuildContext(this.buildContext, this.ormAnnotation, this.tableName);
bool isNotCustomExprField(FieldElement field) {
var col = columns[field.name];
return col?.hasExpression != true;
}
Iterable<FieldElement> get effectiveNormalFields =>
effectiveFields.where(isNotCustomExprField);
} }
class _ColumnType implements ColumnType { class _ColumnType implements ColumnType {

View file

@ -275,6 +275,16 @@ class OrmGenerator extends GeneratorForAnnotation<Orm> {
Code('trampoline.add(tableName);'), Code('trampoline.add(tableName);'),
]); ]);
// Add any manual SQL expressions.
ctx.columns.forEach((name, col) {
if (col != null && col.hasExpression) {
var lhs = refer('expressions').index(
literalString(ctx.buildContext.resolveFieldName(name)));
var rhs = literalString(col.expression);
b.addExpression(lhs.assign(rhs));
}
});
// Add a constructor that initializes _where // Add a constructor that initializes _where
b.addExpression( b.addExpression(
refer('_where') refer('_where')
@ -499,7 +509,8 @@ class OrmGenerator extends GeneratorForAnnotation<Orm> {
..annotations.add(refer('override')) ..annotations.add(refer('override'))
..type = MethodType.getter ..type = MethodType.getter
..body = Block((b) { ..body = Block((b) {
var references = ctx.effectiveFields.map((f) => refer(f.name)); var references =
ctx.effectiveNormalFields.map((f) => refer(f.name));
b.addExpression(literalList(references).returned); b.addExpression(literalList(references).returned);
}); });
})); }));
@ -507,7 +518,7 @@ class OrmGenerator extends GeneratorForAnnotation<Orm> {
var initializers = <Code>[]; var initializers = <Code>[];
// Add builders for each field // Add builders for each field
for (var field in ctx.effectiveFields) { for (var field in ctx.effectiveNormalFields) {
var name = field.name; var name = field.name;
var args = <Expression>[]; var args = <Expression>[];
DartType type; DartType type;
@ -620,7 +631,7 @@ class OrmGenerator extends GeneratorForAnnotation<Orm> {
})); }));
// Each field generates a getter and setter // Each field generates a getter and setter
for (var field in ctx.effectiveFields) { for (var field in ctx.effectiveNormalFields) {
var fType = field.type; var fType = field.type;
var name = ctx.buildContext.resolveFieldName(field.name); var name = ctx.buildContext.resolveFieldName(field.name);
var type = convertTypeReference(field.type); var type = convertTypeReference(field.type);
@ -684,7 +695,7 @@ class OrmGenerator extends GeneratorForAnnotation<Orm> {
..name = 'model' ..name = 'model'
..type = ctx.buildContext.modelClassType)) ..type = ctx.buildContext.modelClassType))
..body = Block((b) { ..body = Block((b) {
for (var field in ctx.effectiveFields) { for (var field in ctx.effectiveNormalFields) {
if (isSpecialId(ctx, field) || field is RelationFieldImpl) { if (isSpecialId(ctx, field) || field is RelationFieldImpl) {
continue; continue;
} }
@ -692,7 +703,7 @@ class OrmGenerator extends GeneratorForAnnotation<Orm> {
.assign(refer('model').property(field.name))); .assign(refer('model').property(field.name)));
} }
for (var field in ctx.effectiveFields) { for (var field in ctx.effectiveNormalFields) {
if (field is RelationFieldImpl) { if (field is RelationFieldImpl) {
var original = field.originalFieldName; var original = field.originalFieldName;
var prop = refer('model').property(original); var prop = refer('model').property(original);

View file

@ -15,6 +15,6 @@ dev_dependencies:
path: ../angel_orm_test path: ../angel_orm_test
pretty_logging: ^1.0.0 pretty_logging: ^1.0.0
test: ^1.0.0 test: ^1.0.0
# dependency_overrides: dependency_overrides:
# angel_orm: angel_orm:
# path: ../angel_orm path: ../angel_orm

View file

@ -12,6 +12,8 @@ void main() {
group('postgresql', () { group('postgresql', () {
group('belongsTo', group('belongsTo',
() => belongsToTests(pg(['author', 'book']), close: closePg)); () => belongsToTests(pg(['author', 'book']), close: closePg));
group('customExpr',
() => customExprTests(pg(['custom_expr']), close: closePg));
group( group(
'edgeCase', 'edgeCase',
() => edgeCaseTests(pg(['unorthodox', 'weird_join', 'song', 'numba']), () => edgeCaseTests(pg(['unorthodox', 'weird_join', 'song', 'numba']),

View file

@ -0,0 +1,13 @@
CREATE TEMPORARY TABLE "numbers" (
id serial PRIMARY KEY,
created_at timestamp,
updated_at timestamp
);
CREATE TEMPORARY TABLE "alphabets" (
id serial PRIMARY KEY,
value TEXT,
numbers_id int,
created_at timestamp,
updated_at timestamp
);

View file

@ -1,4 +1,5 @@
export 'src/belongs_to_test.dart'; export 'src/belongs_to_test.dart';
export 'src/custom_expr_test.dart';
export 'src/edge_case_test.dart'; export 'src/edge_case_test.dart';
export 'src/enum_and_nested_test.dart'; export 'src/enum_and_nested_test.dart';
export 'src/has_many_test.dart'; export 'src/has_many_test.dart';

View file

@ -0,0 +1,43 @@
import 'dart:async';
import 'package:angel_orm/angel_orm.dart';
import 'package:test/test.dart';
import 'models/custom_expr.dart';
import 'util.dart';
customExprTests(FutureOr<QueryExecutor> Function() createExecutor,
{FutureOr<void> Function(QueryExecutor) close}) {
QueryExecutor executor;
Numbers numbersModel;
close ??= (_) => null;
setUp(() async {
executor = await createExecutor();
var now = DateTime.now();
var nQuery = NumbersQuery();
nQuery.values
..createdAt = now
..updatedAt = now;
numbersModel = await nQuery.insert(executor);
});
tearDown(() => close(executor));
test('fetches correct result', () async {
expect(numbersModel.two, 2);
});
test('in relation', () async {
var abcQuery = AlphabetQuery();
abcQuery.values
..value = 'abc'
..numbersId = numbersModel.idAsInt
..createdAt = numbersModel.createdAt
..updatedAt = numbersModel.updatedAt;
var abc = await abcQuery.insert(executor);
expect(abc.numbers, numbersModel);
expect(abc.numbers.two, 2);
expect(abc.value, 'abc');
});
}

View file

@ -0,0 +1,21 @@
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 'custom_expr.g.dart';
@serializable
@orm
class _Numbers extends Model {
@Column(expression: 'SELECT 2')
int two;
}
@serializable
@orm
class _Alphabet extends Model {
String value;
@belongsTo
_Numbers numbers;
}

View file

@ -0,0 +1,538 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'custom_expr.dart';
// **************************************************************************
// MigrationGenerator
// **************************************************************************
class NumbersMigration extends Migration {
@override
up(Schema schema) {
schema.create('numbers', (table) {
table.serial('id')..primaryKey();
table.timeStamp('created_at');
table.timeStamp('updated_at');
});
}
@override
down(Schema schema) {
schema.drop('numbers');
}
}
class AlphabetMigration extends Migration {
@override
up(Schema schema) {
schema.create('alphabets', (table) {
table.serial('id')..primaryKey();
table.timeStamp('created_at');
table.timeStamp('updated_at');
table.varChar('value');
table
.declare('numbers_id', ColumnType('serial'))
.references('numbers', 'id');
});
}
@override
down(Schema schema) {
schema.drop('alphabets');
}
}
// **************************************************************************
// OrmGenerator
// **************************************************************************
class NumbersQuery extends Query<Numbers, NumbersQueryWhere> {
NumbersQuery({Query parent, Set<String> trampoline}) : super(parent: parent) {
trampoline ??= Set();
trampoline.add(tableName);
expressions['two'] = 'SELECT 2';
_where = NumbersQueryWhere(this);
}
@override
final NumbersQueryValues values = NumbersQueryValues();
NumbersQueryWhere _where;
@override
get casts {
return {};
}
@override
get tableName {
return 'numbers';
}
@override
get fields {
return const ['id', 'created_at', 'updated_at', 'two'];
}
@override
NumbersQueryWhere get where {
return _where;
}
@override
NumbersQueryWhere newWhereClause() {
return NumbersQueryWhere(this);
}
static Numbers parseRow(List row) {
if (row.every((x) => x == null)) return null;
var model = Numbers(
id: row[0].toString(),
createdAt: (row[1] as DateTime),
updatedAt: (row[2] as DateTime),
two: (row[3] as int));
return model;
}
@override
deserialize(List row) {
return parseRow(row);
}
}
class NumbersQueryWhere extends QueryWhere {
NumbersQueryWhere(NumbersQuery query)
: id = NumericSqlExpressionBuilder<int>(query, 'id'),
createdAt = DateTimeSqlExpressionBuilder(query, 'created_at'),
updatedAt = DateTimeSqlExpressionBuilder(query, 'updated_at');
final NumericSqlExpressionBuilder<int> id;
final DateTimeSqlExpressionBuilder createdAt;
final DateTimeSqlExpressionBuilder updatedAt;
@override
get expressionBuilders {
return [id, createdAt, updatedAt];
}
}
class NumbersQueryValues extends MapQueryValues {
@override
get casts {
return {};
}
String get id {
return (values['id'] as String);
}
set id(String value) => values['id'] = value;
DateTime get createdAt {
return (values['created_at'] as DateTime);
}
set createdAt(DateTime value) => values['created_at'] = value;
DateTime get updatedAt {
return (values['updated_at'] as DateTime);
}
set updatedAt(DateTime value) => values['updated_at'] = value;
void copyFrom(Numbers model) {
createdAt = model.createdAt;
updatedAt = model.updatedAt;
}
}
class AlphabetQuery extends Query<Alphabet, AlphabetQueryWhere> {
AlphabetQuery({Query parent, Set<String> trampoline})
: super(parent: parent) {
trampoline ??= Set();
trampoline.add(tableName);
_where = AlphabetQueryWhere(this);
leftJoin(_numbers = NumbersQuery(trampoline: trampoline, parent: this),
'numbers_id', 'id',
additionalFields: const ['id', 'created_at', 'updated_at', 'two'],
trampoline: trampoline);
}
@override
final AlphabetQueryValues values = AlphabetQueryValues();
AlphabetQueryWhere _where;
NumbersQuery _numbers;
@override
get casts {
return {};
}
@override
get tableName {
return 'alphabets';
}
@override
get fields {
return const ['id', 'created_at', 'updated_at', 'value', 'numbers_id'];
}
@override
AlphabetQueryWhere get where {
return _where;
}
@override
AlphabetQueryWhere newWhereClause() {
return AlphabetQueryWhere(this);
}
static Alphabet parseRow(List row) {
if (row.every((x) => x == null)) return null;
var model = Alphabet(
id: row[0].toString(),
createdAt: (row[1] as DateTime),
updatedAt: (row[2] as DateTime),
value: (row[3] as String));
if (row.length > 5) {
model = model.copyWith(
numbers: NumbersQuery.parseRow(row.skip(5).take(4).toList()));
}
return model;
}
@override
deserialize(List row) {
return parseRow(row);
}
NumbersQuery get numbers {
return _numbers;
}
}
class AlphabetQueryWhere extends QueryWhere {
AlphabetQueryWhere(AlphabetQuery query)
: id = NumericSqlExpressionBuilder<int>(query, 'id'),
createdAt = DateTimeSqlExpressionBuilder(query, 'created_at'),
updatedAt = DateTimeSqlExpressionBuilder(query, 'updated_at'),
value = StringSqlExpressionBuilder(query, 'value'),
numbersId = NumericSqlExpressionBuilder<int>(query, 'numbers_id');
final NumericSqlExpressionBuilder<int> id;
final DateTimeSqlExpressionBuilder createdAt;
final DateTimeSqlExpressionBuilder updatedAt;
final StringSqlExpressionBuilder value;
final NumericSqlExpressionBuilder<int> numbersId;
@override
get expressionBuilders {
return [id, createdAt, updatedAt, value, numbersId];
}
}
class AlphabetQueryValues extends MapQueryValues {
@override
get casts {
return {};
}
String get id {
return (values['id'] as String);
}
set id(String value) => values['id'] = value;
DateTime get createdAt {
return (values['created_at'] as DateTime);
}
set createdAt(DateTime value) => values['created_at'] = value;
DateTime get updatedAt {
return (values['updated_at'] as DateTime);
}
set updatedAt(DateTime value) => values['updated_at'] = value;
String get value {
return (values['value'] as String);
}
set value(String value) => values['value'] = value;
int get numbersId {
return (values['numbers_id'] as int);
}
set numbersId(int value) => values['numbers_id'] = value;
void copyFrom(Alphabet model) {
createdAt = model.createdAt;
updatedAt = model.updatedAt;
value = model.value;
if (model.numbers != null) {
values['numbers_id'] = model.numbers.id;
}
}
}
// **************************************************************************
// JsonModelGenerator
// **************************************************************************
@generatedSerializable
class Numbers extends _Numbers {
Numbers({this.id, this.createdAt, this.updatedAt, this.two});
/// A unique identifier corresponding to this item.
@override
String id;
/// The time at which this item was created.
@override
DateTime createdAt;
/// The last time at which this item was updated.
@override
DateTime updatedAt;
@override
int two;
Numbers copyWith(
{String id, DateTime createdAt, DateTime updatedAt, int two}) {
return Numbers(
id: id ?? this.id,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
two: two ?? this.two);
}
bool operator ==(other) {
return other is _Numbers &&
other.id == id &&
other.createdAt == createdAt &&
other.updatedAt == updatedAt &&
other.two == two;
}
@override
int get hashCode {
return hashObjects([id, createdAt, updatedAt, two]);
}
@override
String toString() {
return "Numbers(id=$id, createdAt=$createdAt, updatedAt=$updatedAt, two=$two)";
}
Map<String, dynamic> toJson() {
return NumbersSerializer.toMap(this);
}
}
@generatedSerializable
class Alphabet extends _Alphabet {
Alphabet({this.id, this.createdAt, this.updatedAt, this.value, this.numbers});
/// A unique identifier corresponding to this item.
@override
String id;
/// The time at which this item was created.
@override
DateTime createdAt;
/// The last time at which this item was updated.
@override
DateTime updatedAt;
@override
String value;
@override
_Numbers numbers;
Alphabet copyWith(
{String id,
DateTime createdAt,
DateTime updatedAt,
String value,
_Numbers numbers}) {
return Alphabet(
id: id ?? this.id,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
value: value ?? this.value,
numbers: numbers ?? this.numbers);
}
bool operator ==(other) {
return other is _Alphabet &&
other.id == id &&
other.createdAt == createdAt &&
other.updatedAt == updatedAt &&
other.value == value &&
other.numbers == numbers;
}
@override
int get hashCode {
return hashObjects([id, createdAt, updatedAt, value, numbers]);
}
@override
String toString() {
return "Alphabet(id=$id, createdAt=$createdAt, updatedAt=$updatedAt, value=$value, numbers=$numbers)";
}
Map<String, dynamic> toJson() {
return AlphabetSerializer.toMap(this);
}
}
// **************************************************************************
// SerializerGenerator
// **************************************************************************
const NumbersSerializer numbersSerializer = NumbersSerializer();
class NumbersEncoder extends Converter<Numbers, Map> {
const NumbersEncoder();
@override
Map convert(Numbers model) => NumbersSerializer.toMap(model);
}
class NumbersDecoder extends Converter<Map, Numbers> {
const NumbersDecoder();
@override
Numbers convert(Map map) => NumbersSerializer.fromMap(map);
}
class NumbersSerializer extends Codec<Numbers, Map> {
const NumbersSerializer();
@override
get encoder => const NumbersEncoder();
@override
get decoder => const NumbersDecoder();
static Numbers fromMap(Map map) {
return Numbers(
id: map['id'] as String,
createdAt: map['created_at'] != null
? (map['created_at'] is DateTime
? (map['created_at'] as DateTime)
: DateTime.parse(map['created_at'].toString()))
: null,
updatedAt: map['updated_at'] != null
? (map['updated_at'] is DateTime
? (map['updated_at'] as DateTime)
: DateTime.parse(map['updated_at'].toString()))
: null,
two: map['two'] as int);
}
static Map<String, dynamic> toMap(_Numbers model) {
if (model == null) {
return null;
}
return {
'id': model.id,
'created_at': model.createdAt?.toIso8601String(),
'updated_at': model.updatedAt?.toIso8601String(),
'two': model.two
};
}
}
abstract class NumbersFields {
static const List<String> allFields = <String>[id, createdAt, updatedAt, two];
static const String id = 'id';
static const String createdAt = 'created_at';
static const String updatedAt = 'updated_at';
static const String two = 'two';
}
const AlphabetSerializer alphabetSerializer = AlphabetSerializer();
class AlphabetEncoder extends Converter<Alphabet, Map> {
const AlphabetEncoder();
@override
Map convert(Alphabet model) => AlphabetSerializer.toMap(model);
}
class AlphabetDecoder extends Converter<Map, Alphabet> {
const AlphabetDecoder();
@override
Alphabet convert(Map map) => AlphabetSerializer.fromMap(map);
}
class AlphabetSerializer extends Codec<Alphabet, Map> {
const AlphabetSerializer();
@override
get encoder => const AlphabetEncoder();
@override
get decoder => const AlphabetDecoder();
static Alphabet fromMap(Map map) {
return Alphabet(
id: map['id'] as String,
createdAt: map['created_at'] != null
? (map['created_at'] is DateTime
? (map['created_at'] as DateTime)
: DateTime.parse(map['created_at'].toString()))
: null,
updatedAt: map['updated_at'] != null
? (map['updated_at'] is DateTime
? (map['updated_at'] as DateTime)
: DateTime.parse(map['updated_at'].toString()))
: null,
value: map['value'] as String,
numbers: map['numbers'] != null
? NumbersSerializer.fromMap(map['numbers'] as Map)
: null);
}
static Map<String, dynamic> toMap(_Alphabet model) {
if (model == null) {
return null;
}
return {
'id': model.id,
'created_at': model.createdAt?.toIso8601String(),
'updated_at': model.updatedAt?.toIso8601String(),
'value': model.value,
'numbers': NumbersSerializer.toMap(model.numbers)
};
}
}
abstract class AlphabetFields {
static const List<String> allFields = <String>[
id,
createdAt,
updatedAt,
value,
numbers
];
static const String id = 'id';
static const String createdAt = 'created_at';
static const String updatedAt = 'updated_at';
static const String value = 'value';
static const String numbers = 'numbers';
}