buildQueryClass method
Generate
Implementation
Class buildQueryClass(OrmBuildContext ctx) {
return Class((clazz) {
var rc = ctx.buildContext.modelClassNameRecase;
var queryWhereType = refer('${rc.pascalCase}QueryWhere');
log.info('Generating ${rc.pascalCase}QueryWhere');
var nullableQueryWhereType = TypeReference((b) => b
..symbol = '${rc.pascalCase}QueryWhere'
..isNullable = true);
clazz
..name = '${rc.pascalCase}Query'
..extend = TypeReference((b) {
b
..symbol = 'Query'
..types.addAll([
ctx.buildContext.modelClassType,
queryWhereType,
]);
});
// Override casts so that we can cast doubles
clazz.methods.add(Method((b) {
b
..name = 'casts'
..annotations.add(refer('override'))
..returns = TypeReference((b) => b
..symbol = 'Map'
..types.add(refer('String'))
..types.add(refer('String')))
..type = MethodType.getter
..body = Block((b) {
var args = <String, Expression>{};
/* Remove casts no numeric values
for (var field in ctx.effectiveFields) {
var name = ctx.buildContext.resolveFieldName(field.name);
var type = ctx.columns[field.name]?.type;
if (type == null) continue;
if (floatTypes.contains(type)) {
//args[name] = literalString('text');
args[name!] = literalString('char');
}
}
*/
b.addExpression(literalMap(args).returned);
});
}));
// Add values
clazz.fields.add(Field((b) {
var type = refer('${rc.pascalCase}QueryValues');
b
..name = 'values'
..modifier = FieldModifier.final$
..annotations.add(refer('override'))
..type = type
..assignment = type.newInstance([]).code;
}));
// Add tableName
clazz.methods.add(Method((m) {
m
..name = 'tableName'
..returns = refer('String')
..annotations.add(refer('override'))
..type = MethodType.getter
..body = Block((b) {
b.addExpression(literalString(ctx.tableName!).returned);
});
}));
// Add fields getter
clazz.methods.add(Method((m) {
//log.fine('Field: $name');
m
..name = 'fields'
..returns = TypeReference((b) => b
..symbol = 'List'
..types.add(TypeReference((b) => b..symbol = 'String')))
..annotations.add(refer('override'))
..type = MethodType.getter
..body = Block((b) {
var names = ctx.effectiveFields
.map((f) =>
literalString(ctx.buildContext.resolveFieldName(f.name)!))
.toList();
//b.addExpression(literalConstList(names).assignConst('_fields'));
b.addExpression(
declareConst('_fields').assign(literalConstList(names)));
b.addExpression(refer('_selectedFields')
.property('isEmpty')
.conditional(
refer('_fields'),
refer('_fields')
.property('where')
.call([
CodeExpression(
Code('(field) => _selectedFields.contains(field)'))
])
.property('toList')
.call([]),
)
.returned);
});
}));
// Add _selectedFields member
clazz.fields.add(Field((b) {
b
..name = '_selectedFields'
..type = TypeReference((t) => t
..symbol = 'List'
..types.add(TypeReference((b) => b..symbol = 'String')))
..assignment = Code('[]');
}));
// Add select(List<String> fields)
clazz.methods.add(Method((m) {
m
..name = 'select'
..returns = refer('${rc.pascalCase}Query')
..requiredParameters.add(Parameter((b) => b
..name = 'selectedFields'
..type = TypeReference((t) => t
..symbol = 'List'
..types.add(TypeReference((b) => b..symbol = 'String')))))
..body = Block((b) {
b.addExpression(
refer('_selectedFields').assign(refer('selectedFields')),
);
b.addExpression(refer('this').returned);
});
}));
// Add _where member
clazz.fields.add(Field((b) {
b
..name = '_where'
..type = nullableQueryWhereType;
}));
// Add where getter
clazz.methods.add(Method((b) {
b
..name = 'where'
..type = MethodType.getter
..returns = nullableQueryWhereType
..annotations.add(refer('override'))
..body = Block((b) => b.addExpression(refer('_where').returned));
}));
// newWhereClause()
clazz.methods.add(Method((b) {
b
..name = 'newWhereClause'
..annotations.add(refer('override'))
..returns = queryWhereType
..body = Block((b) => b.addExpression(
queryWhereType.newInstance([refer('this')]).returned));
}));
// Add parseRow()
clazz.methods.add(Method((m) {
m
..name = 'parseRow'
..returns = refer('Optional<${rc.pascalCase}>')
..requiredParameters.add(Parameter((b) => b
..name = 'row'
..type = refer('List')))
..body = Block((b) {
var i = 0;
// Build the arguments for model
var args = <String, Expression>{};
for (var field in ctx.effectiveFields) {
var fType = field.type;
Reference type = convertTypeReference(fType);
if (isSpecialId(ctx, field)) {
type = refer('int');
}
// Generated Code: row[i]
var expr = (refer('row').index(literalNum(i++)));
if (isSpecialId(ctx, field)) {
// Generated Code: row[i].toString()
expr = expr.property('toString').call([]);
} else if (field is RelationFieldImpl) {
continue;
} else if (ctx.columns[field.name]?.type == ColumnType.json) {
expr = refer('json')
.property('decode')
.call([expr.asA(refer('String'))]).asA(type);
} else if (floatTypes.contains(ctx.columns[field.name]?.type)) {
//expr = refer('double')
// .property('tryParse')
// .call([expr.property('toString').call([])]);
expr = refer('mapToDouble').call([expr]);
} else if (fType is InterfaceType &&
fType.element is EnumElement) {
/*
* fields.contains('type') ? row[3] == null ? null :
* EnumType.values[(row[3] as int)] : null,
*/
var isNull = expr.equalTo(literalNull);
final parseExpression = _deserializeEnumExpression(field, expr);
expr = isNull.conditional(literalNull, parseExpression);
} else if (fType.isDartCoreBool) {
// Generated Code: mapToBool(row[i])
expr = refer('mapToBool').call([expr]);
} else if (fType.element?.displayName == 'DateTime') {
// Generated Code: mapToDateTime(row[i])
if (fType.nullabilitySuffix == NullabilitySuffix.question) {
expr = refer('mapToNullableDateTime').call([expr]);
} else {
expr = refer('mapToDateTime').call([expr]);
}
} else {
// Generated Code: (row[i] as type?)
expr = expr.asA(type);
}
Expression defaultRef = refer('null');
if (fType.nullabilitySuffix != NullabilitySuffix.question) {
if (fType.isDartCoreString) {
defaultRef = CodeExpression(Code('\'\''));
} else if (fType.isDartCoreBool) {
defaultRef = CodeExpression(Code('false'));
} else if (fType.isDartCoreDouble) {
defaultRef = CodeExpression(Code('0.0'));
} else if (fType.isDartCoreInt || fType.isDartCoreNum) {
defaultRef = CodeExpression(Code('0'));
} else if (fType.element?.displayName == 'DateTime') {
defaultRef = CodeExpression(
Code('DateTime.parse("1970-01-01 00:00:00")'));
} else if (fType.isDartCoreList) {
defaultRef = CodeExpression(Code('[]'));
}
}
expr = refer('fields').property('contains').call([
literalString(ctx.buildContext.resolveFieldName(field.name)!)
]).conditional(expr, defaultRef);
args[field.name] = expr;
}
b.statements.add(Code(
'if (row.every((x) => x == null)) { return Optional.empty(); }'));
//b.addExpression(refer('0').assignVar('_index'));
//b.addExpression(ctx.buildContext.modelClassType
// .newInstance([], args).assignVar('model'));
b.addExpression(declareVar('model')
.assign(ctx.buildContext.modelClassType.newInstance([], args)));
ctx.relations.forEach((name, relation) {
if (!const [
RelationshipType.hasOne,
RelationshipType.belongsTo,
RelationshipType.hasMany
].contains(relation.type)) {
//log.warning('Unsupported relationship for field $name');
return;
}
//log.fine('Process relationship');
var foreign = relation.foreign;
if (foreign == null) {
log.warning('Foreign relationship for field $name is null');
return;
}
//log.fine('Detected relationship ${RelationshipType.belongsTo}');
var skipToList = refer('row')
.property('skip')
.call([literalNum(i)])
.property('take')
.call([literalNum(foreign.effectiveFields.length)])
.property('toList')
.call([]);
var parsed = refer(
'${foreign.buildContext.modelClassNameRecase.pascalCase}Query')
.newInstance([])
.property('parseRow')
.call([skipToList]);
//var baseClass = '${foreign.buildContext.originalClassName}';
//var modelClass =
// foreign.buildContext.modelClassNameRecase.pascalCase;
// Assume: baseclass starts with "_"
//'_${foreign.buildContext.modelClassNameRecase.pascalCase}';
//if (relation.type == RelationshipType.hasMany) {
// parsed = literalList([parsed.asA(refer(modelClass))]);
//parsed = literalList([parsed.asA(refer(baseClass))]);
// var pp = parsed.accept(DartEmitter(useNullSafetySyntax: true));
// parsed = CodeExpression(Code('$pp'));
//Code('$pp.where((x) => x != null).toList()'));
//}
//var expr =
// refer('model').property('copyWith').call([], {name: parsed});
//var block =
// Block((b) => b.addExpression(refer('model').assign(expr)));
var stmt = declareVar('modelOpt').assign(parsed);
//parsed.assignVar('modelOpt');
//var e = refer('Optional').property('ifPresent').call([]);
var val =
(relation.type == RelationshipType.hasMany) ? '[m]' : 'm';
var code = Code('''
modelOpt.ifPresent((m) {
model = model.copyWith($name: $val);
})
''');
var block = Block((b) {
b.addExpression(stmt);
b.addExpression(CodeExpression(code));
});
var blockStr =
block.accept(DartEmitter(useNullSafetySyntax: true));
var ifStr = 'if (row.length > $i) { $blockStr }';
b.statements.add(Code(ifStr));
i += foreign.effectiveFields.length;
});
b.addExpression(
refer('Optional.of').call([refer('model')]).returned);
});
}));
// deserialize
clazz.methods.add(Method((m) {
m
..name = 'deserialize'
..returns = refer('Optional<${rc.pascalCase}>')
..annotations.add(refer('override'))
..requiredParameters.add(Parameter((b) => b
..name = 'row'
..type = refer('List')))
..body = Block((b) {
b.addExpression(refer('parseRow').call([refer('row')]).returned);
});
}));
// If there are any relations, we need some overrides.
clazz.constructors.add(Constructor((b) {
b
..optionalParameters.add(Parameter((b) => b
..named = true
..name = 'parent'
..type = refer('Query?')))
..optionalParameters.add(Parameter((b) => b
..named = true
..name = 'trampoline'
..type = refer('Set<String>?')))
//TypeReference((b) => b
//..symbol = 'Set'
//..types.add(refer('String')))))
..initializers.add(Code('super(parent: parent)'))
..body = Block((b) {
b.statements.addAll([
Code('trampoline ??= <String>{};'),
Code('trampoline.add(tableName);'),
]);
// Add any manual SQL expressions.
ctx.columns.forEach((name, col) {
if (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
b.addExpression(
refer('_where')
.assign(queryWhereType.newInstance([refer('this')])),
);
// Note: this is where subquery fields for relations are added.
ctx.relations.forEach((fieldName, relation) {
//var name = ctx.buildContext.resolveFieldName(fieldName);
if (relation.type == RelationshipType.belongsTo ||
relation.type == RelationshipType.hasOne ||
relation.type == RelationshipType.hasMany) {
//
// @ManyToMany(_RoleUser) => relation.throughContext
// List<_User> get users; => relation.foreign
//
var relationForeign = relation.foreign;
if (relationForeign == null) {
log.warning('$fieldName has no relationship in the context');
return;
}
var relationContext =
relation.throughContext ?? relation.foreign;
//log.fine(
// '$fieldName relation.throughContext => ${relation.throughContext?.tableName} relation.foreign => ${relation.foreign?.tableName}');
// If this is a many-to-many, add the fields from the other object.
var additionalStrs = relationForeign.effectiveFields.map((f) =>
relationForeign.buildContext.resolveFieldName(f.name));
var additionalFields = <Expression>[];
for (var element in additionalStrs) {
if (element != null) {
additionalFields.add(literalString(element));
}
}
var joinArgs = <Expression>[];
for (var element in [relation.localKey, relation.foreignKey]) {
if (element != null) {
joinArgs.add(literalString(element));
}
}
// In the case of a many-to-many, we don't generate a subquery field,
// as it easily leads to stack overflows.
if (relation.isManyToMany) {
// We can't simply join against the "through" table; this itself must
// be a join.
// (SELECT role_users.role_id, <user_fields>
// FROM users
// LEFT JOIN role_users ON role_users.user_id=users.id)
var foreignFields = additionalStrs
.map((f) => '${relationForeign.tableName}.$f');
var b = StringBuffer('(SELECT ');
// role_users.role_id
b.write('${relationContext?.tableName}');
b.write('.${relation.foreignKey}');
// , <user_fields>
b.write(foreignFields.isEmpty
? ''
: ', ${foreignFields.join(', ')}');
// FROM users
b.write(' FROM ');
b.write(relationForeign.tableName);
// LEFT JOIN role_users
b.write(' LEFT JOIN ${relationContext?.tableName}');
// Figure out which field on the "through" table points to users (foreign)
//log.fine('$fieldName query => ${b.toString()}');
var throughRelation =
relationContext?.relations.values.firstWhere((e) {
//log.fine(
// 'ForeignTable(Rel) => ${e.foreignTable}, ${relationForeign.tableName}');
return e.foreignTable == relationForeign.tableName;
}, orElse: () {
// _Role has a many-to-many to _User through _RoleUser, but
// _RoleUser has no relation pointing to _User.
var b = StringBuffer();
b.write(ctx.buildContext.modelClassName);
b.write(' has a many-to-many relationship to ');
b.write(relationForeign.buildContext.modelClassName);
b.write(' through ');
b.write(relationContext.buildContext.modelClassName);
b.write(', but ');
b.write(relationContext.buildContext.modelClassName);
b.write(' has no relation pointing to ');
b.write(ctx.buildContext.modelClassName);
b.write('.');
throw b.toString();
});
// ON role_users.user_id=users.id)
b.write(' ON ');
b.write('${relation.throughContext!.tableName}');
b.write('.');
b.write(throughRelation?.localKey);
b.write('=');
b.write(relationForeign.tableName);
b.write('.');
b.write(throughRelation?.foreignKey);
b.write(')');
joinArgs.insert(0, literalString(b.toString()));
} else {
// In the past, we would either do a join on the table name
// itself, or create an instance of a query.
//
// From this point on, however, we will create a field for each
// join, so that users can customize the generated query.
//
// There'll be a private `_field`, and then a getter, named `field`,
// that returns the subquery object.
var foreignQueryType = refer(
'${relationForeign.buildContext.modelClassNameRecase.pascalCase}Query');
clazz
..fields.add(Field((b) => b
..name = '_$fieldName'
..late = true
..type = foreignQueryType))
..methods.add(Method((b) => b
..name = fieldName
..type = MethodType.getter
..returns = foreignQueryType
..body = refer('_$fieldName').returned.statement));
// Assign a value to `_field`.
var queryInstantiation = foreignQueryType.newInstance([], {
'trampoline': refer('trampoline'),
'parent': refer('this')
});
joinArgs.insert(
0, refer('_$fieldName').assign(queryInstantiation));
}
var joinType = relation.joinTypeString;
b.addExpression(refer(joinType).call(joinArgs, {
'additionalFields':
literalConstList(additionalFields.toList()),
'trampoline': refer('trampoline'),
}));
}
});
});
}));
// If we have any many-to-many relations, we need to prevent
// fetching this table within their joins.
var manyToMany = ctx.relations.entries.where((e) => e.value.isManyToMany);
if (manyToMany.isNotEmpty) {
var outExprs = manyToMany.map<Expression>((e) {
var foreignTableName = e.value.throughContext!.tableName;
return CodeExpression(Code('''
(!(
trampoline.contains('${ctx.tableName}')
&& trampoline.contains('$foreignTableName')
))
'''));
});
var out = outExprs.reduce((a, b) => a.and(b));
clazz.methods.add(Method((b) {
b
..name = 'canCompile'
..annotations.add(refer('override'))
..requiredParameters.add(Parameter((b) => b..name = 'trampoline'))
..returns = refer('bool')
..body = Block((b) {
b.addExpression(out.returned);
});
}));
}
// Also, if there is a @HasMany, generate overrides for query methods that
// execute in a transaction, and invoke fetchLinked.
if (ctx.relations.values.any((r) => r.type == RelationshipType.hasMany)) {
for (var methodName in const ['get', 'update', 'delete']) {
clazz.methods.add(Method((b) {
var type = ctx.buildContext.modelClassType
.accept(DartEmitter(useNullSafetySyntax: true));
b
..name = methodName
..returns = TypeReference((b) => b
..symbol = 'Future'
..types.add(TypeReference((b) => b
..symbol = 'List'
..types.add(TypeReference((b) => b
..symbol = '$type'
..isNullable = false)))))
..annotations.add(refer('override'))
..requiredParameters.add(Parameter((b) => b
..name = 'executor'
..type = refer('QueryExecutor')));
// Collect hasMany options, and ultimately merge them
var merge = <String>[];
ctx.relations.forEach((name, relation) {
if (relation.type == RelationshipType.hasMany) {
// This is only allowed with lists.
var field =
ctx.buildContext.fields.firstWhere((f) => f.name == name);
var typeLiteral = convertTypeReference(field.type)
.accept(DartEmitter(useNullSafetySyntax: true))
.toString()
.replaceAll('?', '');
merge.add('''
$name: $typeLiteral.from(l.$name)..addAll(model.$name)
''');
}
});
var merged = merge.join(', ');
var keyName =
findPrimaryFieldInList(ctx, ctx.buildContext.fields)?.name;
if (keyName == null) {
throw '${ctx.buildContext.originalClassName} has no defined primary key.\n'
'@HasMany and @ManyToMany relations require a primary key to be defined on the model.';
}
b.body = Code('''
return super.$methodName(executor).then((result) {
return result.fold<List<$type>>([], (out, model) {
var idx = out.indexWhere((m) => m.$keyName == model.$keyName);
if (idx == -1) {
return out..add(model);
} else {
var l = out[idx];
return out..[idx] = l.copyWith($merged);
}
});
});
''');
}));
}
}
});
}