mirror of https://github.com/AMT-Cheif/drift.git
925 lines
30 KiB
Dart
925 lines
30 KiB
Dart
import 'package:drift/drift.dart';
|
|
import 'package:recase/recase.dart';
|
|
import 'package:sqlparser/sqlparser.dart' hide ResultColumn;
|
|
|
|
import '../../analysis/resolver/queries/nested_queries.dart';
|
|
import '../../analysis/results/results.dart';
|
|
import '../../analysis/options.dart';
|
|
import '../../analysis/resolver/queries/explicit_alias_transformer.dart';
|
|
import '../../utils/string_escaper.dart';
|
|
import '../writer.dart';
|
|
import 'result_set_writer.dart';
|
|
import 'sql_writer.dart';
|
|
import 'utils.dart';
|
|
|
|
const highestAssignedIndexVar = '\$arrayStartIndex';
|
|
|
|
typedef _ArgumentContext = ({
|
|
// Indicates that the argument is available under a prefix in SQL, probably
|
|
// because it comes from a [NestedResultTable].
|
|
String? sqlPrefix,
|
|
// Indicates that, even if the argument appears to be non-nullable by itself,
|
|
// it comes from a [NestedResultTable] part of an outer join that could make
|
|
// the entire structure nullable.
|
|
bool isNullable,
|
|
});
|
|
|
|
/// Writes the handling code for a query. The code emitted will be a method that
|
|
/// should be included in a generated database or dao class.
|
|
class QueryWriter {
|
|
final Scope scope;
|
|
|
|
late final ExplicitAliasTransformer _transformer;
|
|
final TextEmitter _emitter;
|
|
StringBuffer get _buffer => _emitter.buffer;
|
|
|
|
DriftOptions get options => scope.writer.options;
|
|
|
|
QueryWriter(this.scope) : _emitter = scope.leaf();
|
|
|
|
void write(SqlQuery query) {
|
|
// Note that writing queries can have a result set if they use a RETURNING
|
|
// clause.
|
|
final resultSet = query.resultSet;
|
|
if (resultSet?.needsOwnClass == true) {
|
|
final resultSetScope = scope.root.child();
|
|
ResultSetWriter(query, resultSetScope).write();
|
|
}
|
|
|
|
// We generate the Dart string literal for the SQL query by walking the
|
|
// parsed AST. This eliminates unnecessary whitespace and comments in the
|
|
// generated code.
|
|
// In some cases, the whitespace has an impact on the semantic of the
|
|
// query. For instance, `SELECT 1 + 2` has a different column name than
|
|
// `SELECT 1+2`. To work around this, we transform the query to add an
|
|
// explicit alias to every column (since whitespace doesn't matter if the
|
|
// query is written as `SELECT 1+2 AS c0`).
|
|
// We do this transformation so late because it shouldn't have an impact on
|
|
// analysis, Dart getter names stay the same.
|
|
if (resultSet != null) {
|
|
_transformer = ExplicitAliasTransformer();
|
|
_transformer.rewrite(query.root!);
|
|
|
|
final nested = query is SqlSelectQuery ? query.nestedContainer : null;
|
|
if (nested != null) {
|
|
addHelperNodes(nested);
|
|
}
|
|
}
|
|
|
|
if (query is SqlSelectQuery) {
|
|
_writeSelect(query);
|
|
} else if (query is UpdatingQuery) {
|
|
if (resultSet != null) {
|
|
_writeUpdatingQueryWithReturning(query);
|
|
} else {
|
|
_writeUpdatingQuery(query);
|
|
}
|
|
}
|
|
}
|
|
|
|
void _writeSelect(SqlSelectQuery select) {
|
|
_writeSelectStatementCreator(select);
|
|
}
|
|
|
|
String _nameOfCreationMethod(SqlSelectQuery select) => select.name;
|
|
|
|
/// Writes the function literal that turns a "QueryRow" into the desired
|
|
/// custom return type of a query.
|
|
void _writeMappingLambda(InferredResultSet resultSet, QueryRowType rowClass) {
|
|
final queryRow = _emitter.drift('QueryRow');
|
|
final asyncModifier = rowClass.requiresAsynchronousContext ? 'async' : '';
|
|
|
|
// We can write every available mapping as a Dart expression via
|
|
// _writeArgumentExpression. This can be turned into a lambda by appending
|
|
// it with `(QueryRow row) => $expression`. That's also what we're doing,
|
|
// but if we'll just call mapFromRow in there, we can just tear that method
|
|
// off instead. This is just an optimization.
|
|
final singleValue = rowClass.singleValue;
|
|
if (singleValue is MatchingDriftTable && singleValue.effectivelyNoAlias) {
|
|
// Tear-off mapFromRow method on table
|
|
_emitter.write('${singleValue.table.dbGetterName}.mapFromRow');
|
|
} else {
|
|
// In all other cases, we're off to write the expression.
|
|
_emitter.write('($queryRow row) $asyncModifier => ');
|
|
_writeArgumentExpression(
|
|
rowClass, resultSet, (sqlPrefix: null, isNullable: false));
|
|
}
|
|
}
|
|
|
|
/// Writes code that will read the [argument] for an existing row type from
|
|
/// the raw `QueryRow`.
|
|
void _writeArgumentExpression(
|
|
ArgumentForQueryRowType argument,
|
|
InferredResultSet resultSet,
|
|
_ArgumentContext context,
|
|
) {
|
|
switch (argument) {
|
|
case RawQueryRow():
|
|
_buffer.write('row');
|
|
case ScalarResultColumn():
|
|
_readScalar(argument, context);
|
|
case MatchingDriftTable():
|
|
_readMatchingTable(argument, context);
|
|
case StructuredFromNestedColumn():
|
|
final prefix = resultSet.nestedPrefixFor(argument.table);
|
|
_writeArgumentExpression(
|
|
argument.nestedType,
|
|
resultSet,
|
|
(sqlPrefix: prefix, isNullable: argument.nullable),
|
|
);
|
|
case MappedNestedListQuery():
|
|
_buffer.write('await ');
|
|
final query = argument.column.query;
|
|
_writeCustomSelectStatement(query, argument.nestedType);
|
|
_buffer.write('.get()');
|
|
case QueryRowType():
|
|
final singleValue = argument.singleValue;
|
|
if (singleValue != null) {
|
|
return _writeArgumentExpression(singleValue, resultSet, context);
|
|
}
|
|
|
|
if (context.isNullable) {
|
|
// If this structed type is nullable, it's coming from an OUTER join
|
|
// which means that, even if the individual components making up the
|
|
// structure are non-nullable, they might all be null in SQL. We
|
|
// detect this case by looking for a non-nullable column and, if it's
|
|
// null, return null directly instead of creating the structured type.
|
|
for (final arg in argument.positionalArguments
|
|
.followedBy(argument.namedArguments.values)
|
|
.whereType<ScalarResultColumn>()) {
|
|
if (!arg.nullable) {
|
|
final keyInMap = context.applyPrefix(arg.name);
|
|
_buffer.write(
|
|
'row.data[${asDartLiteral(keyInMap)}] == null ? null : ');
|
|
}
|
|
}
|
|
}
|
|
|
|
final _ArgumentContext childContext = (
|
|
sqlPrefix: context.sqlPrefix,
|
|
// Individual fields making up this query row type aren't covered by
|
|
// the outer nullability.
|
|
isNullable: false,
|
|
);
|
|
|
|
if (!argument.isRecord) {
|
|
// We're writing a constructor, so let's start with the class name.
|
|
_emitter.writeDart(argument.rowType);
|
|
|
|
final constructorName = argument.constructorName;
|
|
if (constructorName.isNotEmpty) {
|
|
_emitter
|
|
..write('.')
|
|
..write(constructorName);
|
|
}
|
|
}
|
|
|
|
_buffer.write('(');
|
|
for (final positional in argument.positionalArguments) {
|
|
_writeArgumentExpression(positional, resultSet, childContext);
|
|
_buffer.write(', ');
|
|
}
|
|
argument.namedArguments.forEach((name, parameter) {
|
|
_buffer.write('$name: ');
|
|
_writeArgumentExpression(parameter, resultSet, childContext);
|
|
_buffer.write(', ');
|
|
});
|
|
|
|
_buffer.write(')');
|
|
}
|
|
}
|
|
|
|
/// Writes Dart code that, given a variable of type `QueryRow` named `row`
|
|
/// in the same scope, reads the [column] from that row and brings it into a
|
|
/// suitable type.
|
|
void _readScalar(ScalarResultColumn column, _ArgumentContext context) {
|
|
final specialName = _transformer.newNameFor(column.sqlParserColumn!);
|
|
final isNullable = context.isNullable || column.nullable;
|
|
|
|
var name = specialName ?? column.name;
|
|
if (context.sqlPrefix != null) {
|
|
name = '${context.sqlPrefix}.$name';
|
|
}
|
|
|
|
final dartLiteral = asDartLiteral(name);
|
|
|
|
final rawDartType =
|
|
_emitter.dartCode(_emitter.innerColumnType(column.sqlType));
|
|
String code;
|
|
|
|
if (column.sqlType.isCustom) {
|
|
final method = isNullable ? 'readNullableWithType' : 'readWithType';
|
|
final typeImpl = _emitter.dartCode(column.sqlType.custom!.expression);
|
|
code = 'row.$method<$rawDartType>($typeImpl, $dartLiteral)';
|
|
} else {
|
|
final method = isNullable ? 'readNullable' : 'read';
|
|
code = 'row.$method<$rawDartType>($dartLiteral)';
|
|
}
|
|
|
|
final converter = column.typeConverter;
|
|
if (converter != null) {
|
|
if (converter.canBeSkippedForNulls && isNullable) {
|
|
// The type converter maps non-nullable types, but the column may be
|
|
// nullable in SQL => just map null to null and only invoke the type
|
|
// converter for non-null values.
|
|
final wrapFrom = _emitter.drift('NullAwareTypeConverter.wrapFromSql');
|
|
code = '$wrapFrom(${readConverter(_emitter, converter)}, $code)';
|
|
} else {
|
|
// Just apply the type converter directly.
|
|
code = '${readConverter(_emitter, converter)}.fromSql($code)';
|
|
}
|
|
}
|
|
|
|
_emitter.write(code);
|
|
}
|
|
|
|
void _readMatchingTable(MatchingDriftTable match, _ArgumentContext context) {
|
|
// note that, even if the result set has a matching table, we can't just
|
|
// use the mapFromRow() function of that table - the column names might
|
|
// be different!
|
|
final table = match.table;
|
|
|
|
if (match.effectivelyNoAlias) {
|
|
final mappingMethod =
|
|
context.isNullable ? 'mapFromRowOrNull' : 'mapFromRow';
|
|
final sqlPrefix = context.sqlPrefix;
|
|
|
|
_emitter.write('await ${table.dbGetterName}.$mappingMethod(row');
|
|
if (sqlPrefix != null) {
|
|
_emitter.write(', tablePrefix: ${asDartLiteral(sqlPrefix)}');
|
|
}
|
|
|
|
_emitter.write(')');
|
|
} else {
|
|
// If the entire table can be nullable, we can check whether a non-nullable
|
|
// column from the table is null. If it is, the entire table is null. This
|
|
// can happen when the table comes from an outer join.
|
|
if (context.isNullable) {
|
|
for (final MapEntry(:key, :value) in match.aliasToColumn.entries) {
|
|
if (!value.nullable) {
|
|
final mapKey = context.applyPrefix(key);
|
|
|
|
_emitter
|
|
.write('row.data[${asDartLiteral(mapKey)}] == null ? null : ');
|
|
}
|
|
}
|
|
}
|
|
|
|
_emitter.write('${table.dbGetterName}.mapFromRowWithAlias(row, const {');
|
|
|
|
for (final alias in match.aliasToColumn.entries) {
|
|
_emitter
|
|
..write(asDartLiteral(context.applyPrefix(alias.key)))
|
|
..write(': ')
|
|
..write(asDartLiteral(alias.value.nameInSql))
|
|
..write(', ');
|
|
}
|
|
|
|
_emitter.write('})');
|
|
}
|
|
}
|
|
|
|
/// Writes a method returning a `Selectable<T>`, where `T` is the return type
|
|
/// of the custom query.
|
|
void _writeSelectStatementCreator(SqlSelectQuery select) {
|
|
final returnType = AnnotatedDartCode.build((builder) {
|
|
builder
|
|
..addSymbol('Selectable', AnnotatedDartCode.drift)
|
|
..addText('<')
|
|
..addQueryResultRowType(select)
|
|
..addText('>');
|
|
});
|
|
|
|
final methodName = _nameOfCreationMethod(select);
|
|
|
|
_emitter
|
|
..writeDart(returnType)
|
|
..write(' $methodName(');
|
|
_writeParameters(select);
|
|
_buffer.write(') {\n');
|
|
|
|
_writeExpandedDeclarations(select);
|
|
_buffer.write('return');
|
|
_writeCustomSelectStatement(select);
|
|
_buffer.write(';\n}\n');
|
|
}
|
|
|
|
void _writeCustomSelectStatement(SqlSelectQuery select,
|
|
[QueryRowType? resultType]) {
|
|
_buffer.write(' customSelect(${_queryCode(select)}, ');
|
|
_writeVariables(select);
|
|
_buffer.write(', ');
|
|
_writeReadsFrom(select);
|
|
|
|
final resultSet = select.resultSet;
|
|
resultType ??= select.queryRowType(options);
|
|
|
|
if (resultType.requiresAsynchronousContext) {
|
|
_buffer.write(').asyncMap(');
|
|
} else {
|
|
_buffer.write(').map(');
|
|
}
|
|
|
|
_writeMappingLambda(resultSet, resultType);
|
|
_buffer.write(')');
|
|
}
|
|
|
|
void _writeUpdatingQueryWithReturning(UpdatingQuery update) {
|
|
final type = AnnotatedDartCode.build((builder) {
|
|
builder
|
|
..addSymbol('Future', AnnotatedDartCode.dartAsync)
|
|
..addText('<')
|
|
..addSymbol('List', AnnotatedDartCode.dartCore)
|
|
..addText('<')
|
|
..addQueryResultRowType(update)
|
|
..addText('>>');
|
|
});
|
|
|
|
_emitter
|
|
..writeDart(type)
|
|
..write(' ${update.name}(');
|
|
_writeParameters(update);
|
|
_buffer.write(') {\n');
|
|
|
|
_writeExpandedDeclarations(update);
|
|
_buffer.write('return customWriteReturning(${_queryCode(update)},');
|
|
_writeCommonUpdateParameters(update);
|
|
|
|
_buffer.write(').then((rows) => ');
|
|
|
|
final resultSet = update.resultSet!;
|
|
final rowType = update.queryRowType(options);
|
|
|
|
if (rowType.requiresAsynchronousContext) {
|
|
_buffer.write('Future.wait(rows.map(');
|
|
_writeMappingLambda(resultSet, rowType);
|
|
_buffer.write('))');
|
|
} else {
|
|
_buffer.write('rows.map(');
|
|
_writeMappingLambda(resultSet, rowType);
|
|
_buffer.write(').toList()');
|
|
}
|
|
_buffer.write(');\n}');
|
|
}
|
|
|
|
void _writeUpdatingQuery(UpdatingQuery update) {
|
|
/*
|
|
Future<int> test() {
|
|
return customUpdate('', variables: [], updates: {});
|
|
}
|
|
*/
|
|
final implName = update.isInsert ? 'customInsert' : 'customUpdate';
|
|
|
|
_buffer.write('Future<int> ${update.name}(');
|
|
_writeParameters(update);
|
|
_buffer.write(') {\n');
|
|
|
|
_writeExpandedDeclarations(update);
|
|
_buffer.write('return $implName(${_queryCode(update)},');
|
|
_writeCommonUpdateParameters(update);
|
|
_buffer.write(',);\n}\n');
|
|
}
|
|
|
|
void _writeCommonUpdateParameters(UpdatingQuery update) {
|
|
_writeVariables(update);
|
|
_buffer.write(',');
|
|
_writeUpdates(update);
|
|
_writeUpdateKind(update);
|
|
}
|
|
|
|
void _writeParameters(SqlQuery query) {
|
|
final namedElements = <FoundElement>[];
|
|
|
|
String scopedTypeName(FoundDartPlaceholder element) {
|
|
return '${ReCase(query.name).pascalCase}\$${ReCase(element.name).camelCase}';
|
|
}
|
|
|
|
String typeFor(FoundElement element) {
|
|
return _emitter.dartCode(element.dartType(scope));
|
|
}
|
|
|
|
String writeScopedTypeFor(FoundDartPlaceholder element) {
|
|
final root = scope.root;
|
|
final type = typeFor(element);
|
|
final scopedType = scopedTypeName(element);
|
|
|
|
final args = element.availableResultSets
|
|
.map((e) =>
|
|
'${_emitter.dartCode(scope.entityInfoType(e.entity))} ${e.name}')
|
|
.join(', ');
|
|
root.leaf().write('typedef $scopedType = $type Function($args);');
|
|
|
|
return scopedType;
|
|
}
|
|
|
|
var needsComma = false;
|
|
for (final element in query.elementsWithNestedQueries()) {
|
|
if (element.hidden) continue;
|
|
|
|
// Placeholders with a default value generate optional (and thus, named)
|
|
// parameters. Since moor 4, we have an option to also generate named
|
|
// parameters for named variables.
|
|
final isNamed = (element is FoundDartPlaceholder && element.hasDefault) ||
|
|
(element.hasSqlName && options.generateNamedParameters);
|
|
|
|
if (isNamed) {
|
|
namedElements.add(element);
|
|
} else {
|
|
if (needsComma) _buffer.write(', ');
|
|
|
|
var type = typeFor(element);
|
|
if (element is FoundDartPlaceholder &&
|
|
element.writeAsScopedFunction(options)) {
|
|
type = writeScopedTypeFor(element);
|
|
}
|
|
_buffer.write('$type ${element.dartParameterName}');
|
|
needsComma = true;
|
|
}
|
|
}
|
|
|
|
// Write optional placeholder as named arguments
|
|
if (namedElements.isNotEmpty) {
|
|
if (needsComma) _buffer.write(', ');
|
|
_buffer.write('{');
|
|
needsComma = false;
|
|
|
|
for (final optional in namedElements) {
|
|
if (needsComma) _buffer.write(', ');
|
|
needsComma = true;
|
|
|
|
String? defaultCode;
|
|
var isNullable = false;
|
|
var type = typeFor(optional);
|
|
|
|
if (optional is FoundDartPlaceholder) {
|
|
// If optional Dart placeholders are written as functions, they are
|
|
// generated as nullable parameters. The default is handled with a
|
|
// `??` in the method's body.
|
|
if (optional.writeAsScopedFunction(options)) {
|
|
isNullable = optional.hasDefaultOrImplicitFallback;
|
|
type = writeScopedTypeFor(optional);
|
|
|
|
if (isNullable) {
|
|
type += '?';
|
|
}
|
|
} else {
|
|
defaultCode = _defaultForDartPlaceholder(optional, scope);
|
|
}
|
|
}
|
|
|
|
// No default value, this element is required if it's not nullable
|
|
var isMarkedAsRequired = false;
|
|
if (optional is FoundVariable) {
|
|
isMarkedAsRequired = optional.isRequired;
|
|
isNullable = optional.nullableInDart;
|
|
}
|
|
final isRequired =
|
|
(!isNullable || isMarkedAsRequired) && defaultCode == null ||
|
|
options.namedParametersAlwaysRequired;
|
|
if (isRequired) {
|
|
_buffer.write('required ');
|
|
}
|
|
|
|
_buffer.write('$type ${optional.dartParameterName}');
|
|
if (defaultCode != null && !isRequired) {
|
|
_buffer
|
|
..write(' = ')
|
|
..write(defaultCode);
|
|
}
|
|
}
|
|
|
|
_buffer.write('}');
|
|
}
|
|
}
|
|
|
|
void _writeExpandedDeclarations(SqlQuery query) {
|
|
_ExpandedDeclarationWriter(query, options, scope, _buffer)
|
|
.writeExpandedDeclarations();
|
|
}
|
|
|
|
void _writeVariables(SqlQuery query) {
|
|
_ExpandedVariableWriter(query, _emitter).writeVariables();
|
|
}
|
|
|
|
/// Returns a Dart string literal representing the query after variables have
|
|
/// been expanded. For instance, 'SELECT * FROM t WHERE x IN ?' will be turned
|
|
/// into 'SELECT * FROM t WHERE x IN ($expandedVar1)'.
|
|
String _queryCode(SqlQuery query) {
|
|
final dialectForCode = <String, List<SqlDialect>>{};
|
|
|
|
for (final dialect in scope.options.supportedDialects) {
|
|
final code =
|
|
SqlWriter(scope.options, dialect: dialect, query: query).write();
|
|
|
|
dialectForCode.putIfAbsent(code, () => []).add(dialect);
|
|
}
|
|
|
|
if (dialectForCode.length == 1) {
|
|
// All supported dialects use the same SQL syntax, so we can just use that
|
|
return dialectForCode.keys.single;
|
|
} else {
|
|
// Create a switch expression matching over the dialect of the database
|
|
// we're connected to.
|
|
final buffer = StringBuffer('switch (executor.dialect) {');
|
|
final dialectEnum = scope.drift('SqlDialect');
|
|
|
|
var index = 0;
|
|
for (final MapEntry(key: code, value: dialects)
|
|
in dialectForCode.entries) {
|
|
index++;
|
|
|
|
buffer
|
|
.write(dialects.map((e) => '$dialectEnum.${e.name}').join(' || '));
|
|
if (index == dialectForCode.length) {
|
|
// In the last branch, match all dialects as a fallback
|
|
buffer.write(' || _ ');
|
|
}
|
|
|
|
buffer
|
|
..write(' => ')
|
|
..write(code)
|
|
..write(', ');
|
|
}
|
|
|
|
buffer.writeln('}');
|
|
return buffer.toString();
|
|
}
|
|
}
|
|
|
|
void _writeReadsFrom(SqlSelectQuery select) {
|
|
_buffer.write('readsFrom: {');
|
|
|
|
for (final table in select.readsFromTables) {
|
|
_buffer.write('${table.dbGetterName},');
|
|
}
|
|
|
|
for (final element in select
|
|
.elementsWithNestedQueries()
|
|
.whereType<FoundDartPlaceholder>()) {
|
|
_buffer.write('...${placeholderContextName(element)}.watchedTables,');
|
|
}
|
|
|
|
_buffer.write('}');
|
|
}
|
|
|
|
void _writeUpdates(UpdatingQuery update) {
|
|
final from = update.updates.map((t) => t.table.dbGetterName).join(', ');
|
|
_buffer
|
|
..write('updates: {')
|
|
..write(from)
|
|
..write('}');
|
|
}
|
|
|
|
void _writeUpdateKind(UpdatingQuery update) {
|
|
if (update.isOnlyDelete) {
|
|
_buffer.write(', updateKind: ${_emitter.drift('UpdateKind.delete')}');
|
|
} else if (update.isOnlyUpdate) {
|
|
_buffer.write(', updateKind: ${_emitter.drift('UpdateKind.update')}');
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns code to load an instance of the [converter] at runtime.
|
|
String readConverter(TextEmitter emitter, AppliedTypeConverter converter) {
|
|
return emitter.dartCode(emitter.readConverter(converter));
|
|
}
|
|
|
|
class _ExpandedDeclarationWriter {
|
|
final SqlQuery query;
|
|
final DriftOptions options;
|
|
final Scope _scope;
|
|
final StringBuffer _buffer;
|
|
|
|
bool indexCounterWasDeclared = false;
|
|
bool needsIndexCounter = false;
|
|
int highestIndexBeforeArray = 0;
|
|
|
|
_ExpandedDeclarationWriter(
|
|
this.query, this.options, this._scope, this._buffer);
|
|
|
|
void writeExpandedDeclarations() {
|
|
// When the SQL query is written to a Dart string, we give each variable an
|
|
// eplixit index (e.g `?2`), regardless of how it was declared in the
|
|
// source.
|
|
// Array variables are converted into multiple variables at runtime, but
|
|
// let's give variables before that an index, all other variables can be
|
|
// turned into explicit indices though. We ensure that array variables have
|
|
// higher indices than other variables.
|
|
var index = 0;
|
|
for (final variable in query.variables) {
|
|
if (!variable.isArray) {
|
|
// Re-assign continous indices to non-array variables
|
|
highestIndexBeforeArray = variable.index = ++index;
|
|
}
|
|
}
|
|
|
|
needsIndexCounter = true;
|
|
for (final element in query.elementsWithNestedQueries()) {
|
|
if (element is FoundVariable) {
|
|
if (element.isArray) {
|
|
_writeArrayVariable(element);
|
|
}
|
|
} else if (element is FoundDartPlaceholder) {
|
|
_writeDartPlaceholder(element);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Some notes on parameters and generating query code:
|
|
// We expand array parameters to multiple variables at runtime (see the
|
|
// documentation of FoundVariable and SqlQuery for further discussion).
|
|
// To do this. we have to rewrite the sql. Consider this query:
|
|
// SELECT * FROM t WHERE a = ?1 AND b IN :vars OR c IN :vars AND d = ?
|
|
// When expanding an array variable, we write the expanded sql into a local
|
|
// var called "expanded$Name", e.g. when we bind "vars" to [1, 2, 3] in the
|
|
// query, then `expandedVars` would be "(?2, ?3, ?4)".
|
|
// We use explicit indexes when expanding so that we don't have to expand the
|
|
// "vars" variable twice. To do this, a local var called "$currentVarIndex"
|
|
// keeps track of the highest variable number assigned.
|
|
// We can use the same mechanism for runtime Dart placeholders, where we
|
|
// generate a GenerationContext, write the placeholder and finally extract the
|
|
// variables
|
|
//
|
|
// For the new query generation mode, we instead give every regular variable
|
|
// an explicit index and then append arrays and other constructs after these
|
|
// indices have been written.
|
|
|
|
void _writeIndexCounterIfNeeded() {
|
|
if (indexCounterWasDeclared || !needsIndexCounter) {
|
|
return; // already written or not necessary at all
|
|
}
|
|
|
|
// we only need the index counter when the query contains an expanded
|
|
// element.
|
|
// add +1 because that's going to be the first index of this element.
|
|
final firstVal = highestIndexBeforeArray + 1;
|
|
_buffer.write('var $highestAssignedIndexVar = $firstVal;');
|
|
indexCounterWasDeclared = true;
|
|
}
|
|
|
|
void _increaseIndexCounter(String by) {
|
|
if (needsIndexCounter) {
|
|
_buffer
|
|
..write('$highestAssignedIndexVar += ')
|
|
..write(by)
|
|
..write(';\n');
|
|
}
|
|
}
|
|
|
|
void _writeDartPlaceholder(FoundDartPlaceholder element) {
|
|
String useExpression() {
|
|
if (element.writeAsScopedFunction(options)) {
|
|
// The parameter is a function type that needs to be evaluated first
|
|
final args = element.availableResultSets.map((e) {
|
|
final table = 'this.${e.entity.dbGetterName}';
|
|
final needsAlias = e.name != e.entity.schemaName;
|
|
|
|
if (needsAlias) {
|
|
return 'alias($table, ${asDartLiteral(e.name)})';
|
|
} else {
|
|
return table;
|
|
}
|
|
}).join(', ');
|
|
|
|
final defaultValue = _defaultForDartPlaceholder(element, _scope);
|
|
|
|
if (defaultValue != null) {
|
|
// Optional elements written as a function are generated as nullable
|
|
// parameters. We need to emit the default if the actual value is
|
|
// null at runtime.
|
|
return '${element.dartParameterName}?.call($args) ?? $defaultValue';
|
|
} else {
|
|
return '${element.dartParameterName}($args)';
|
|
}
|
|
} else {
|
|
// We can just use the parameter directly
|
|
return element.dartParameterName;
|
|
}
|
|
}
|
|
|
|
_writeIndexCounterIfNeeded();
|
|
_buffer
|
|
..write('final ')
|
|
..write(placeholderContextName(element))
|
|
..write(' = ');
|
|
|
|
final type = element.type;
|
|
if (type is InsertableDartPlaceholderType) {
|
|
final table = type.table;
|
|
|
|
_buffer
|
|
..write(r'$writeInsertable(this.')
|
|
..write(table?.dbGetterName)
|
|
..write(', ')
|
|
..write(useExpression())
|
|
..write(', startIndex: $highestAssignedIndexVar');
|
|
|
|
_buffer.write(');\n');
|
|
} else {
|
|
_buffer
|
|
..write(r'$write(')
|
|
..write(useExpression());
|
|
|
|
if (element.availableResultSets.length > 1) {
|
|
_buffer.write(', hasMultipleTables: true');
|
|
}
|
|
|
|
_buffer
|
|
..write(', startIndex: $highestAssignedIndexVar')
|
|
..write(');\n');
|
|
}
|
|
|
|
// similar to the case for expanded array variables, we need to
|
|
// increase the index
|
|
_increaseIndexCounter(
|
|
'${placeholderContextName(element)}.amountOfVariables');
|
|
}
|
|
|
|
void _writeArrayVariable(FoundVariable element) {
|
|
assert(element.isArray);
|
|
|
|
_writeIndexCounterIfNeeded();
|
|
|
|
// final expandedvar1 = $expandVar(<startIndex>, <amount>);
|
|
_buffer
|
|
..write('final ')
|
|
..write(expandedName(element))
|
|
..write(' = ')
|
|
..write(r'$expandVar(')
|
|
..write(highestAssignedIndexVar)
|
|
..write(', ')
|
|
..write(element.dartParameterName)
|
|
..write('.length);\n');
|
|
|
|
// increase highest index for the next expanded element
|
|
_increaseIndexCounter('${element.dartParameterName}.length');
|
|
}
|
|
}
|
|
|
|
class _ExpandedVariableWriter {
|
|
final SqlQuery query;
|
|
final TextEmitter _emitter;
|
|
StringBuffer get _buffer => _emitter.buffer;
|
|
|
|
_ExpandedVariableWriter(this.query, this._emitter);
|
|
|
|
void writeVariables() {
|
|
_buffer.write('variables: ');
|
|
|
|
// Some dialects don't support variables with an explicit index. In that
|
|
// case, we have to desugar them by duplicating variables, e.g. `:a AND :a`
|
|
// would be transformed to `? AND ?` with us binding the value to both
|
|
// variables.
|
|
if (_emitter.writer.options.supportedDialects
|
|
.any((e) => !e.supportsIndexedParameters) &&
|
|
query.referencesAnyElementMoreThanOnce) {
|
|
_buffer.write('executor.dialect.desugarDuplicateVariables([');
|
|
_writeNewVariables();
|
|
_buffer.write('],');
|
|
|
|
// Every time a variable is used in the generated SQL text, we have to
|
|
// track the variable's index in the second list
|
|
_buffer.write('[');
|
|
for (final source in query.elementSources) {
|
|
switch (source.referencedElement) {
|
|
case FoundVariable variable:
|
|
_buffer.write('${variable.index}, ');
|
|
break;
|
|
case FoundDartPlaceholder placeholder:
|
|
final context = placeholderContextName(placeholder);
|
|
_buffer.write('...$context.variableIndices');
|
|
break;
|
|
}
|
|
}
|
|
|
|
_buffer.write('])');
|
|
} else {
|
|
_buffer.write('[');
|
|
_writeNewVariables();
|
|
_buffer.write(']');
|
|
}
|
|
}
|
|
|
|
void _writeNewVariables() {
|
|
// In the new generation mode, we first write all non-array variables in
|
|
// a continuous block, then we proceed to add arrays and other expanded
|
|
// declarations.
|
|
var first = true;
|
|
|
|
for (final variable in query.variables) {
|
|
if (!variable.isArray) {
|
|
if (!first) {
|
|
_buffer.write(', ');
|
|
}
|
|
|
|
_writeElement(variable);
|
|
first = false;
|
|
}
|
|
}
|
|
|
|
for (final element in query.elements) {
|
|
final shouldBeWritten = element is! FoundVariable || element.isArray;
|
|
|
|
if (shouldBeWritten) {
|
|
if (!first) {
|
|
_buffer.write(', ');
|
|
}
|
|
|
|
_writeElement(element);
|
|
first = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
void _writeElement(FoundElement element) {
|
|
if (element is FoundVariable) {
|
|
_writeVariable(element);
|
|
} else if (element is FoundDartPlaceholder) {
|
|
_writeDartPlaceholder(element);
|
|
}
|
|
}
|
|
|
|
void _writeVariable(FoundVariable element) {
|
|
// Variables without type converters are written as:
|
|
// `Variable<int>(x)`. When there's a type converter, we instead use
|
|
// `Variable<int>(typeConverter.toSql(x))`.
|
|
// Finally, if we're dealing with a list, we use a collection for to
|
|
// write all the variables sequentially.
|
|
String constructVar(String dartExpr) {
|
|
// No longer an array here, we apply a for loop if necessary
|
|
final type = _emitter
|
|
.dartCode(_emitter.innerColumnType(element.sqlType, nullable: false));
|
|
|
|
final varType = _emitter.drift('Variable');
|
|
final buffer = StringBuffer('$varType<$type>(');
|
|
final capture = element.forCaptured;
|
|
|
|
final converter = element.typeConverter;
|
|
if (converter != null) {
|
|
// Apply the converter.
|
|
if (element.nullable && converter.canBeSkippedForNulls) {
|
|
final nullAware = _emitter.drift('NullAwareTypeConverter');
|
|
|
|
buffer.write('$nullAware.wrapToSql('
|
|
'${readConverter(_emitter, element.typeConverter!)}, $dartExpr)');
|
|
} else {
|
|
buffer.write(
|
|
'${readConverter(_emitter, element.typeConverter!)}.toSql($dartExpr)');
|
|
}
|
|
} else if (capture != null) {
|
|
buffer.write('row.read(${asDartLiteral(capture.helperColumn)})');
|
|
} else {
|
|
buffer.write(dartExpr);
|
|
}
|
|
|
|
buffer.write(')');
|
|
return buffer.toString();
|
|
}
|
|
|
|
final name = element.dartParameterName;
|
|
|
|
if (element.isArray) {
|
|
final constructor = constructVar(r'$');
|
|
_buffer.write('for (var \$ in $name) $constructor');
|
|
} else {
|
|
_buffer.write(constructVar(name));
|
|
}
|
|
}
|
|
|
|
void _writeDartPlaceholder(FoundDartPlaceholder element) {
|
|
_buffer.write('...${placeholderContextName(element)}.introducedVariables');
|
|
}
|
|
}
|
|
|
|
String? _defaultForDartPlaceholder(
|
|
FoundDartPlaceholder placeholder, Scope scope) {
|
|
final kind = placeholder.type;
|
|
if (kind is ExpressionDartPlaceholderType && kind.defaultValue != null) {
|
|
// Wrap the default expression in parentheses to avoid issues with
|
|
// the surrounding precedence in SQL.
|
|
final (sql, dialectSpecific) =
|
|
scope.sqlByDialect(Parentheses(kind.defaultValue!));
|
|
|
|
if (dialectSpecific) {
|
|
return 'const ${scope.drift('CustomExpression')}.dialectSpecific($sql)';
|
|
} else {
|
|
return 'const ${scope.drift('CustomExpression')}($sql)';
|
|
}
|
|
} else if (kind is SimpleDartPlaceholderType &&
|
|
kind.kind == SimpleDartPlaceholderKind.orderBy) {
|
|
return 'const ${scope.drift('OrderBy')}.nothing()';
|
|
} else {
|
|
assert(!placeholder.hasDefaultOrImplicitFallback);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
extension on _ArgumentContext {
|
|
String applyPrefix(String originalName) {
|
|
return switch (sqlPrefix) {
|
|
null => originalName,
|
|
var s => '$s.$originalName',
|
|
};
|
|
}
|
|
}
|