Order all result columns syntactically

This commit is contained in:
Simon Binder 2022-12-26 22:45:35 +01:00
parent 20aff8a9db
commit 44cae65170
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
12 changed files with 238 additions and 181 deletions

View File

@ -77,7 +77,6 @@ Map<SqlSelectQuery, SqlSelectQuery> transformCustomResultClasses(
null,
query.resultSet.columns,
resultClassName: resultSetName,
nestedResults: query.resultSet.nestedResults,
// Only generate a result class for the first query in the group
dontGenerateResultClass: !isFirst,
);
@ -86,7 +85,7 @@ Map<SqlSelectQuery, SqlSelectQuery> transformCustomResultClasses(
// Dart name.
newResultSet.forceDartNames({
for (final entry in dartNames.entries)
newResultSet.columns.singleWhere((e) => e.compatibleTo(entry.key)):
newResultSet.columns.singleWhere((e) => e.isCompatibleTo(entry.key)):
entry.value,
});

View File

@ -2,6 +2,7 @@ import 'package:drift/drift.dart' show DriftSqlType;
import 'package:drift/drift.dart' as drift;
import 'package:recase/recase.dart';
import 'package:sqlparser/sqlparser.dart' hide ResultColumn;
import 'package:sqlparser/sqlparser.dart' as sql;
import 'package:sqlparser/utils/find_referenced_tables.dart';
import '../../driver/driver.dart';
@ -140,8 +141,10 @@ class QueryAnalyzer {
InferredResultSet? resultSet;
if (root is StatementReturningColumns) {
final columns = root.returnedResultSet?.resolvedColumns;
if (columns != null) {
resultSet = _inferResultSet(queryContext, columns);
final syntacticSource = root.returning?.columns;
resultSet = _inferResultSet(queryContext, columns, syntacticSource);
}
}
@ -179,6 +182,7 @@ class QueryAnalyzer {
SqlSelectQuery _handleSelect(_QueryHandlerContext queryContext) {
final tableFinder = ReferencedTablesVisitor();
final root = queryContext.root;
queryContext.root.acceptWithoutArg(tableFinder);
_applyFoundTables(tableFinder);
@ -194,16 +198,20 @@ class QueryAnalyzer {
final driftEntities = [...driftTables, ...driftViews];
final resolvedColumns = (root as BaseSelectStatement).resolvedColumns!;
List<sql.ResultColumn>? syntacticColumns;
if (root is SelectStatement) {
syntacticColumns = root.columns;
}
return SqlSelectQuery(
queryContext.queryName,
context,
queryContext.root,
queryContext.foundElements,
driftEntities,
_inferResultSet(
queryContext,
(queryContext.root as BaseSelectStatement).resolvedColumns!,
),
_inferResultSet(queryContext, resolvedColumns, syntacticColumns),
queryContext.requestedResultClass,
queryContext.nestedScope,
);
@ -212,12 +220,12 @@ class QueryAnalyzer {
InferredResultSet _inferResultSet(
_QueryHandlerContext queryContext,
List<Column> rawColumns,
List<sql.ResultColumn>? syntacticColumns,
) {
final candidatesForSingleTable = {..._foundTables, ..._foundViews};
final columns = <ResultColumn>[];
// First, go through regular result columns
for (final column in rawColumns) {
void handleScalarColumn(Column column) {
final type = context.typeOf(column).type;
final driftType = driver.typeMapping.sqlTypeToDrift(type);
AppliedTypeConverter? converter;
@ -225,23 +233,52 @@ class QueryAnalyzer {
converter = (type!.hint as TypeConverterHint).converter;
}
columns.add(ResultColumn(column.name, driftType, type?.nullable ?? true,
columns.add(ScalarResultColumn(
column.name, driftType, type?.nullable ?? true,
typeConverter: converter, sqlParserColumn: column));
final resultSet = _resultSetOfColumn(column);
candidatesForSingleTable.removeWhere((t) => t != resultSet);
}
final nestedResults = _findNestedResultTables(queryContext);
if (nestedResults.isNotEmpty) {
// The single table optimization doesn't make sense when nested result
// sets are present.
candidatesForSingleTable.clear();
// We prefer to extract the result set from syntactic columns, as this gives
// us the opportunity to get the ordering between "scalar" result columns
// and nested result sets right. We might not have a syntactic source
// though (for instance if a top-level `VALUES` select is used), so we
// fall-back to the resolved schema columns if necessary.
if (syntacticColumns == null) {
rawColumns.forEach(handleScalarColumn);
} else {
for (final column in syntacticColumns) {
final resolvedColumns = column.resolvedColumns;
if (column is NestedStarResultColumn) {
final resolved = _resolveNestedResultTable(queryContext, column);
if (resolved != null) {
// The single table optimization doesn't make sense when nested result
// sets are present.
candidatesForSingleTable.clear();
columns.add(resolved);
}
} else if (column is NestedQueryColumn) {
candidatesForSingleTable.clear();
columns.add(_resolveNestedResultQuery(queryContext, column));
} else {
if (resolvedColumns == null) continue;
// "Regular" column that either is or expands to a list of scalar
// result columns.
for (final column in resolvedColumns) {
handleScalarColumn(column);
}
}
}
}
// if all columns read from the same table, and all columns in that table
// are present in the result set, we can use the data class we generate for
// that table instead of generating another class just for this result set.
if (candidatesForSingleTable.length == 1) {
final table = candidatesForSingleTable.single;
final driftTable =
@ -268,7 +305,7 @@ class QueryAnalyzer {
// it is! Remember the correct getter name from the data class for
// later when we write the mapping code.
final columnIndex = rawColumns.indexOf(inResultSet.single);
final resultColumn = columns[columnIndex];
final resultColumn = columns[columnIndex] as ScalarResultColumn;
resultEntryToColumn[resultColumn] = column.nameInDart;
resultColumnNameToDrift[resultColumn.name] = column;
@ -295,73 +332,75 @@ class QueryAnalyzer {
return InferredResultSet(
null,
columns,
nestedResults: nestedResults,
resultClassName: queryContext.requestedResultClass,
);
}
List<NestedResult> _findNestedResultTables(
_QueryHandlerContext queryContext) {
// We don't currently support nested results for compound statements
if (queryContext.root is! SelectStatement) return const [];
final query = queryContext.root as SelectStatement;
final nestedTables = <NestedResult>[];
final analysis = JoinModel.of(query);
for (final column in query.columns) {
if (column is NestedStarResultColumn) {
final originalResult = column.resultSet;
final result = originalResult?.unalias();
if (result is! Table && result is! View) continue;
final driftTable = _lookupReference<DriftElementWithResultSet>(
(result as NamedResultSet).name);
final isNullable =
analysis == null || analysis.isNullableTable(originalResult!);
nestedTables.add(NestedResultTable(
column,
column.as ?? column.tableName,
driftTable,
isNullable: isNullable,
));
} else if (column is NestedQueryColumn) {
final childScope = queryContext.nestedScope?.nestedQueries[column];
final foundElements = _extractElements(
ctx: context,
root: column.select,
required: requiredVariables,
nestedScope: childScope,
);
_verifyNoSkippedIndexes(foundElements);
final queryIndex = nestedQueryCounter++;
final name = 'nested_query_$queryIndex';
column.queryName = name;
var resultClassName = ReCase(queryContext.queryName).pascalCase;
if (column.as != null) {
resultClassName += ReCase(column.as!).pascalCase;
} else {
resultClassName += 'NestedQuery$queryIndex';
}
nestedTables.add(NestedResultQuery(
from: column,
query: _handleSelect(_QueryHandlerContext(
queryName: name,
requestedResultClass: resultClassName,
root: column.select,
foundElements: foundElements,
nestedScope: childScope,
)),
));
}
/// Resolves a "nested star" column.
///
/// Nested star columns refer to an existing result set, but instructs drift
/// that this result set should be handled as a nested type in Dart. For an
/// example, see https://drift.simonbinder.eu/docs/using-sql/drift_files/#nested-results
NestedResultTable? _resolveNestedResultTable(
_QueryHandlerContext queryContext, NestedStarResultColumn column) {
final originalResult = column.resultSet;
final result = originalResult?.unalias();
if (result is! Table && result is! View) {
return null;
}
return nestedTables;
final driftTable = _lookupReference<DriftElementWithResultSet>(
(result as NamedResultSet).name);
final analysis = JoinModel.of(column);
final isNullable =
analysis == null || analysis.isNullableTable(originalResult!);
return NestedResultTable(
column,
column.as ?? column.tableName,
driftTable,
isNullable: isNullable,
);
}
/// Resolves a `LIST` result column.
///
/// The `LIST` macro allows defining a subquery whose results should be
/// exposed as a Dart list.
/// For an example, see https://drift.simonbinder.eu/docs/using-sql/drift_files/#list-subqueries
NestedResultQuery _resolveNestedResultQuery(
_QueryHandlerContext queryContext, NestedQueryColumn column) {
final childScope = queryContext.nestedScope?.nestedQueries[column];
final foundElements = _extractElements(
ctx: context,
root: column.select,
required: requiredVariables,
nestedScope: childScope,
);
_verifyNoSkippedIndexes(foundElements);
final queryIndex = nestedQueryCounter++;
final name = 'nested_query_$queryIndex';
column.queryName = name;
var resultClassName = ReCase(queryContext.queryName).pascalCase;
if (column.as != null) {
resultClassName += ReCase(column.as!).pascalCase;
} else {
resultClassName += 'NestedQuery$queryIndex';
}
return NestedResultQuery(
from: column,
query: _handleSelect(_QueryHandlerContext(
queryName: name,
requestedResultClass: resultClassName,
root: column.select,
foundElements: foundElements,
nestedScope: childScope,
)),
);
}
Column? _toTableOrViewColumn(Column? c) {

View File

@ -1,12 +1,14 @@
import 'package:analyzer/dart/element/type.dart';
import 'package:collection/collection.dart';
import 'package:drift/drift.dart' show DriftSqlType, UpdateKind;
import 'package:meta/meta.dart';
import 'package:recase/recase.dart';
import 'package:sqlparser/sqlparser.dart';
import '../options.dart';
import '../resolver/shared/column_name.dart';
import 'column.dart';
import 'dart.dart';
import 'element.dart';
import 'result_sets.dart';
import 'table.dart';
@ -362,12 +364,13 @@ class InferredResultSet {
/// to create another class.
final MatchingDriftTable? matchingTable;
/// Tables in the result set that should appear as a class.
///
/// See [NestedResult] for further discussion and examples.
final List<NestedResult> nestedResults;
Map<NestedResult, String>? _expandedNestedPrefixes;
/// All columns that are part of this result set.
///
/// This includes [ScalarResultColumn]s, which hold simple SQL values, but
/// also [NestedResult]s, which hold subqueries or tables that are structured
/// into a single logical Dart column.
final List<ResultColumn> columns;
final Map<ResultColumn, String> _dartNames = {};
@ -385,11 +388,13 @@ class InferredResultSet {
InferredResultSet(
this.matchingTable,
this.columns, {
this.nestedResults = const [],
this.resultClassName,
this.dontGenerateResultClass = false,
});
Iterable<ScalarResultColumn> get scalarColumns => columns.whereType();
Iterable<NestedResult> get nestedResults => columns.whereType();
/// Whether a new class needs to be written to store the result of this query.
///
/// We don't need to introduce result classes for queries which
@ -399,14 +404,16 @@ class InferredResultSet {
/// We always need to generate a class if the query contains nested results.
bool get needsOwnClass {
return matchingTable == null &&
(columns.length > 1 || nestedResults.isNotEmpty) &&
(scalarColumns.length > 1 || nestedResults.isNotEmpty) &&
!dontGenerateResultClass;
}
/// Whether this query returns a single column that should be returned
/// directly.
bool get singleColumn =>
matchingTable == null && nestedResults.isEmpty && columns.length == 1;
matchingTable == null &&
nestedResults.isEmpty &&
scalarColumns.length == 1;
String? nestedPrefixFor(NestedResult table) {
if (_expandedNestedPrefixes == null) {
@ -429,8 +436,7 @@ class InferredResultSet {
/// [column].
String dartNameFor(ResultColumn column) {
return _dartNames.putIfAbsent(column, () {
return dartNameForSqlColumn(column.name,
existingNames: _dartNames.values);
return column.dartGetterName(_dartNames.values);
});
}
@ -444,10 +450,7 @@ class InferredResultSet {
/// nested result sets.
bool isCompatibleTo(InferredResultSet other) {
const columnsEquality = UnorderedIterableEquality(_ResultColumnEquality());
const nestedEquality = UnorderedIterableEquality(_NestedResultEquality());
return columnsEquality.equals(columns, other.columns) &&
nestedEquality.equals(nestedResults, other.nestedResults);
return columnsEquality.equals(columns, other.columns);
}
}
@ -471,7 +474,20 @@ class MatchingDriftTable {
}
}
class ResultColumn implements HasType {
@sealed
abstract class ResultColumn {
/// A unique name for this column in Dart.
String dartGetterName(Iterable<String> existingNames);
/// [hashCode] that matches [isCompatibleTo] instead of `==`.
int get compatibilityHashCode;
/// Checks whether this column is compatible to the [other] column, meaning
/// that they have the same name and type.
bool isCompatibleTo(ResultColumn other);
}
class ScalarResultColumn extends ResultColumn implements HasType {
final String name;
@override
final DriftSqlType sqlType;
@ -484,36 +500,36 @@ class ResultColumn implements HasType {
/// The analyzed column from the `sqlparser` package.
final Column? sqlParserColumn;
ResultColumn(this.name, this.sqlType, this.nullable,
ScalarResultColumn(this.name, this.sqlType, this.nullable,
{this.typeConverter, this.sqlParserColumn});
@override
bool get isArray => false;
/// Hash-code that matching [compatibleTo], so that two compatible columns
/// will have the same [compatibilityHashCode].
int get compatibilityHashCode {
return Object.hash(name, sqlType, nullable, typeConverter);
@override
String dartGetterName(Iterable<String> existingNames) {
return dartNameForSqlColumn(name, existingNames: existingNames);
}
/// Checks whether this column is compatible to the [other], meaning that they
/// have the same name and type.
bool compatibleTo(ResultColumn other) {
return other.name == name &&
@override
int get compatibilityHashCode {
return Object.hash(
ScalarResultColumn, name, sqlType, nullable, typeConverter);
}
@override
bool isCompatibleTo(ResultColumn other) {
return other is ScalarResultColumn &&
other.name == name &&
other.sqlType == sqlType &&
other.nullable == nullable &&
other.typeConverter == typeConverter;
}
}
/// A nested result, could either be a NestedResultTable or a NestedQueryResult.
abstract class NestedResult {
/// [hashCode] that matches [isCompatibleTo] instead of `==`.
int get compatibilityHashCode;
/// Checks whether this is compatible to the [other] nested result.
bool isCompatibleTo(NestedResult other);
}
/// A nested result, could either be a [NestedResultTable] or a
/// [NestedResultQuery].
abstract class NestedResult extends ResultColumn {}
/// A nested table extracted from a `**` column.
///
@ -549,7 +565,10 @@ class NestedResultTable extends NestedResult {
NestedResultTable(this.from, this.name, this.table, {this.isNullable = true});
String get dartFieldName => ReCase(name).camelCase;
@override
String dartGetterName(Iterable<String> existingNames) {
return dartNameForSqlColumn(name, existingNames: existingNames);
}
/// [hashCode] that matches [isCompatibleTo] instead of `==`.
@override
@ -560,7 +579,7 @@ class NestedResultTable extends NestedResult {
/// Checks whether this is compatible to the [other] nested result, which is
/// the case iff they have the same and read from the same table.
@override
bool isCompatibleTo(NestedResult other) {
bool isCompatibleTo(ResultColumn other) {
if (other is! NestedResultTable) return false;
return other.name == name &&
@ -579,6 +598,11 @@ class NestedResultQuery extends NestedResult {
required this.query,
});
@override
String dartGetterName(Iterable<String> existingNames) {
return dartNameForSqlColumn(filedName(), existingNames: existingNames);
}
String filedName() {
if (from.as != null) {
return from.as!;
@ -595,7 +619,7 @@ class NestedResultQuery extends NestedResult {
int get compatibilityHashCode => hashCode;
@override
bool isCompatibleTo(NestedResult other) => this == other;
bool isCompatibleTo(ResultColumn other) => this == other;
}
/// Something in the query that needs special attention when generating code,
@ -868,7 +892,7 @@ class _ResultColumnEquality implements Equality<ResultColumn> {
const _ResultColumnEquality();
@override
bool equals(ResultColumn e1, ResultColumn e2) => e1.compatibleTo(e2);
bool equals(ResultColumn e1, ResultColumn e2) => e1.isCompatibleTo(e2);
@override
int hash(ResultColumn e) => e.compatibilityHashCode;
@ -876,18 +900,3 @@ class _ResultColumnEquality implements Equality<ResultColumn> {
@override
bool isValidKey(Object? e) => e is ResultColumn;
}
class _NestedResultEquality implements Equality<NestedResult> {
const _NestedResultEquality();
@override
bool equals(NestedResult e1, NestedResult e2) {
return e1.isCompatibleTo(e2);
}
@override
int hash(NestedResult e) => e.compatibilityHashCode;
@override
bool isValidKey(Object? e) => e is NestedResultTable;
}

View File

@ -79,7 +79,7 @@ class QueryWriter {
final queryRow = _emitter.drift('QueryRow');
if (resultSet.singleColumn) {
final column = resultSet.columns.single;
final column = resultSet.scalarColumns.single;
_buffer.write('($queryRow row) => '
'${readingCode(column, scope.generationOptions, options)}');
} else if (resultSet.matchingTable != null) {
@ -119,31 +119,28 @@ class QueryWriter {
for (final column in resultSet.columns) {
final fieldName = resultSet.dartNameFor(column);
_buffer.write('$fieldName: '
'${readingCode(column, scope.generationOptions, options)},');
}
for (final nested in resultSet.nestedResults) {
if (nested is NestedResultTable) {
final prefix = resultSet.nestedPrefixFor(nested);
if (column is ScalarResultColumn) {
_buffer.write('$fieldName: '
'${readingCode(column, scope.generationOptions, options)},');
} else if (column is NestedResultTable) {
final prefix = resultSet.nestedPrefixFor(column);
if (prefix == null) continue;
final fieldName = nested.dartFieldName;
final tableGetter = nested.table.dbGetterName;
final tableGetter = column.table.dbGetterName;
final mappingMethod =
nested.isNullable ? 'mapFromRowOrNull' : 'mapFromRow';
column.isNullable ? 'mapFromRowOrNull' : 'mapFromRow';
_buffer.write('$fieldName: await $tableGetter.$mappingMethod(row, '
'tablePrefix: ${asDartLiteral(prefix)}),');
} else if (nested is NestedResultQuery) {
final fieldName = nested.filedName();
} else if (column is NestedResultQuery) {
_buffer.write('$fieldName: await ');
_writeCustomSelectStatement(nested.query);
_writeCustomSelectStatement(column.query);
_buffer.write('.get(),');
}
}
_buffer.write(');\n}');
}
}
@ -151,8 +148,8 @@ class QueryWriter {
/// Returns 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.
String readingCode(ResultColumn column, GenerationOptions generationOptions,
DriftOptions moorOptions) {
String readingCode(ScalarResultColumn column,
GenerationOptions generationOptions, DriftOptions moorOptions) {
final specialName = _transformer.newNameFor(column.sqlParserColumn!);
final dartLiteral = asDartLiteral(specialName ?? column.name);

View File

@ -28,39 +28,34 @@ class ResultSetWriter {
final modifier = scope.options.fieldModifier;
// write fields
// Write fields
for (final column in resultSet.columns) {
final name = resultSet.dartNameFor(column);
final runtimeType = into.dartCode(into.dartType(column));
final fieldName = resultSet.dartNameFor(column);
into.write('$modifier $runtimeType $name\n;');
if (column is ScalarResultColumn) {
final runtimeType = into.dartCode(into.dartType(column));
fields.add(EqualityField(name, isList: column.isUint8ListInDart));
if (!column.nullable) nonNullableFields.add(name);
}
for (final nested in resultSet.nestedResults) {
if (nested is NestedResultTable) {
final fieldName = nested.dartFieldName;
into.write('$modifier $runtimeType $fieldName\n;');
fields.add(EqualityField(fieldName, isList: column.isUint8ListInDart));
if (!column.nullable) nonNullableFields.add(fieldName);
} else if (column is NestedResultTable) {
into
..write('$modifier ')
..writeDart(nested.resultRowType(scope))
..write(nested.isNullable ? '? ' : ' ')
..writeDart(column.resultRowType(scope))
..write(column.isNullable ? '? ' : ' ')
..writeln('$fieldName;');
fields.add(EqualityField(fieldName));
if (!nested.isNullable) nonNullableFields.add(fieldName);
} else if (nested is NestedResultQuery) {
final fieldName = nested.filedName();
if (nested.query.resultSet.needsOwnClass) {
ResultSetWriter(nested.query, scope).write();
if (!column.isNullable) nonNullableFields.add(fieldName);
} else if (column is NestedResultQuery) {
if (column.query.resultSet.needsOwnClass) {
ResultSetWriter(column.query, scope).write();
}
into
..write('$modifier ')
..writeDart(nested.resultRowType(scope))
..writeDart(column.resultRowType(scope))
..writeln('$fieldName;');
fields.add(EqualityField(fieldName));

View File

@ -58,7 +58,7 @@ extension SqlQueryType on SqlQuery {
}
if (resultSet.singleColumn) {
return scope.dartType(resultSet.columns.single);
return scope.dartType(resultSet.scalarColumns.single);
}
return AnnotatedDartCode([resultClassName]);

View File

@ -33,7 +33,7 @@ WITH RECURSIVE
expect(resultSet.singleColumn, isTrue);
expect(resultSet.needsOwnClass, isFalse);
expect(resultSet.columns.map(resultSet.dartNameFor), ['x']);
expect(resultSet.columns.map((c) => c.sqlType), [DriftSqlType.int]);
expect(resultSet.scalarColumns.map((c) => c.sqlType), [DriftSqlType.int]);
});
test('finds the underlying table when aliased through CTE', () async {

View File

@ -106,8 +106,8 @@ wrongArgs: SELECT sin(oid, foo) FROM numbers;
final queryInA =
fileA.fileAnalysis!.resolvedQueries.values.single as SqlSelectQuery;
expect(
queryInA.resultSet.columns.single,
const TypeMatcher<ResultColumn>()
queryInA.resultSet.scalarColumns.single,
const TypeMatcher<ScalarResultColumn>()
.having((e) => e.sqlType, 'type', DriftSqlType.double),
);

View File

@ -38,8 +38,8 @@ bar(?1 AS TEXT, :foo AS BOOLEAN): SELECT ?, :foo;
final resultSet = (query as SqlSelectQuery).resultSet;
expect(resultSet.matchingTable, isNull);
expect(resultSet.columns.map((c) => c.name), ['?', ':foo']);
expect(resultSet.columns.map((c) => c.sqlType),
expect(resultSet.scalarColumns.map((c) => c.name), ['?', ':foo']);
expect(resultSet.scalarColumns.map((c) => c.sqlType),
[DriftSqlType.string, DriftSqlType.bool]);
});
@ -161,17 +161,19 @@ q3: SELECT datetime('now');
expect(queries, hasLength(3));
final q1 = queries[0];
expect(q1.resultSet!.columns.single.sqlType, DriftSqlType.dateTime);
expect(q1.resultSet!.scalarColumns.single.sqlType, DriftSqlType.dateTime);
final q2 = queries[1];
final q3 = queries[2];
if (dateTimeAsText) {
expect(q2.resultSet!.columns.single.sqlType, DriftSqlType.int);
expect(q3.resultSet!.columns.single.sqlType, DriftSqlType.dateTime);
expect(q2.resultSet!.scalarColumns.single.sqlType, DriftSqlType.int);
expect(
q3.resultSet!.scalarColumns.single.sqlType, DriftSqlType.dateTime);
} else {
expect(q2.resultSet!.columns.single.sqlType, DriftSqlType.dateTime);
expect(q3.resultSet!.columns.single.sqlType, DriftSqlType.string);
expect(
q2.resultSet!.scalarColumns.single.sqlType, DriftSqlType.dateTime);
expect(q3.resultSet!.scalarColumns.single.sqlType, DriftSqlType.string);
}
});
}
@ -203,7 +205,7 @@ FROM routes
final query = file.fileAnalysis!.resolvedQueries.values.single;
final resultSet = (query as SqlSelectQuery).resultSet;
expect(resultSet.columns.map((e) => e.name), ['id', 'from', 'to']);
expect(resultSet.scalarColumns.map((e) => e.name), ['id', 'from', 'to']);
expect(resultSet.matchingTable, isNull);
expect(
resultSet.nestedResults.cast<NestedResultTable>().map((e) => e.name),
@ -283,7 +285,8 @@ LEFT JOIN tableB1 AS tableB2 -- nullable
final query = result.fileAnalysis!.resolvedQueries.values.single;
expect(query.resultSet!.columns, [
isA<ResultColumn>().having((e) => e.sqlType, 'sqlType', DriftSqlType.bool)
isA<ScalarResultColumn>()
.having((e) => e.sqlType, 'sqlType', DriftSqlType.bool)
]);
final args = query.variables;

View File

@ -195,7 +195,7 @@ class _HasInferredColumnTypes extends CustomMatcher {
final resultSet = actual.resultSet;
return {
for (final column in resultSet.columns) column.name: column.sqlType
for (final column in resultSet.scalarColumns) column.name: column.sqlType
};
}
}

View File

@ -283,8 +283,11 @@ class ColumnResolver extends RecursiveVisitor<void, void> {
}
}
usedColumns
.addAll(visibleColumnsForStar!.where((e) => e.includedInResults));
final added =
visibleColumnsForStar!.where((e) => e.includedInResults).toList();
usedColumns.addAll(added);
resultColumn.resolvedColumns = added;
} else if (resultColumn is ExpressionResultColumn) {
final expression = resultColumn.expression;
Column column;
@ -299,6 +302,7 @@ class ColumnResolver extends RecursiveVisitor<void, void> {
}
usedColumns.add(column);
resultColumn.resolvedColumns = [column];
if (resultColumn.as != null) {
// make this column available for references if there is no other

View File

@ -135,7 +135,18 @@ class ValuesSelectStatement extends BaseSelectStatement
Iterable<AstNode> get childNodes => values;
}
abstract class ResultColumn extends AstNode {}
abstract class ResultColumn extends AstNode {
/// The acutal, schema-level [Column]s that this result column expands to.
///
/// For a [ExpressionResultColumn], this is usually a singleton list with a
/// single [ExpressionColumn]. For star columns, the list may return more
/// elements.
///
/// This list is populated during analysis, it will be `null` after parsing.
/// Further, it may be left in its unset state if the column could not be
/// analyzed due to an error.
List<Column>? resolvedColumns;
}
/// A result column that either yields all columns or all columns from a table
/// by using "*" or "table.*".