Analyze SQL queries

This commit is contained in:
Simon Binder 2022-09-18 21:25:46 +02:00
parent 5947956109
commit b33ef8a220
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
17 changed files with 1947 additions and 53 deletions

View File

@ -8,6 +8,7 @@ import '../drift_native_functions.dart';
import '../resolver/dart/helper.dart';
import '../resolver/discover.dart';
import '../resolver/drift/sqlparser/mapping.dart';
import '../resolver/file_analysis.dart';
import '../resolver/resolver.dart';
import '../results/results.dart';
import '../serializer.dart';
@ -20,7 +21,7 @@ class DriftAnalysisDriver {
final DriftAnalysisCache cache = DriftAnalysisCache();
final DriftOptions options;
late final TypeMapping typeMapping = TypeMapping(options);
late final TypeMapping typeMapping = TypeMapping(this);
late final ElementDeserializer deserializer = ElementDeserializer(this);
AnalysisResultCacheReader? cacheReader;
@ -157,6 +158,22 @@ class DriftAnalysisDriver {
await _analyzePrepared(known);
return known;
}
Future<FileState> fullyAnalyze(Uri uri) async {
// First, make sure that elements in this file and all imports are fully
// resolved.
final state = await resolveElements(uri);
// Then, run local analysis if needed
if (state.fileAnalysis == null) {
final analyzer = FileAnalyzer(this);
final result = await analyzer.runAnalysisOn(state);
state.fileAnalysis = result;
}
return state;
}
}
abstract class AnalysisResultCacheReader {

View File

@ -3,16 +3,18 @@ import 'package:path/path.dart' show url;
import 'package:sqlparser/sqlparser.dart' hide AnalysisError;
import '../results/element.dart';
import '../results/file_results.dart';
import 'error.dart';
class FileState {
final Uri ownUri;
DiscoveredFileState? discovery;
final Map<DriftElementId, ElementAnalysisState> analysis = {};
final List<DriftAnalysisError> errorsDuringDiscovery = [];
final Map<DriftElementId, ElementAnalysisState> analysis = {};
FileAnalysisResult? fileAnalysis;
FileState(this.ownUri);
String get extension => url.extension(ownUri.path);

View File

@ -20,7 +20,7 @@ abstract class DriftElementResolver<T extends DiscoveredElement>
context.errors.forEach(reportLint);
// Also run drift-specific lints on the query
final linter = DriftSqlLinter(context, this)..collectLints();
final linter = DriftSqlLinter(context)..collectLints();
linter.sqlParserErrors.forEach(reportLint);
}
@ -51,18 +51,7 @@ abstract class DriftElementResolver<T extends DiscoveredElement>
}
SqlEngine newEngineWithTables(Iterable<DriftElement> references) {
final driver = resolver.driver;
final engine = driver.newSqlEngine();
for (final reference in references) {
if (reference is DriftTable) {
engine.registerTable(driver.typeMapping.asSqlParserTable(reference));
} else if (reference is DriftView) {
engine.registerView(driver.typeMapping.asSqlParserView(reference));
}
}
return engine;
return resolver.driver.typeMapping.newEngineWithTables(references);
}
Future<List<DriftElement>> resolveSqlReferences(AstNode stmt) async {

View File

@ -12,12 +12,10 @@ class DriftQueryResolver
final stmt = discovered.sqlNode.statement;
final references = await resolveSqlReferences(stmt);
final engine = newEngineWithTables(references);
final source = (file.discovery as DiscoveredDriftFile).originalSource;
final context = engine.analyzeNode(stmt, source);
reportLints(context);
// Note: We don't analyze the query here, that happens in
// `file_analysis.dart` after elements have been resolved.
return DefinedSqlQuery(
discovered.ownId,
DriftDeclaration.driftFile(stmt, file.ownUri),

View File

@ -1,16 +1,13 @@
import 'package:sqlparser/sqlparser.dart';
import '../../resolver.dart';
/// Implements (mostly drift-specific) lints for SQL statements that aren't
/// implementeed in `sqlparser`.
class DriftSqlLinter {
final AnalysisContext _context;
final LocalElementResolver _resolver;
final List<AnalysisError> sqlParserErrors = [];
DriftSqlLinter(this._context, this._resolver);
DriftSqlLinter(this._context);
void collectLints() {
_context.root.acceptWithoutArg(_LintingVisitor(this));

View File

@ -1,15 +1,29 @@
import 'package:drift/drift.dart' show DriftSqlType;
import 'package:sqlparser/sqlparser.dart';
import '../../../../analyzer/options.dart';
import '../../../driver/driver.dart';
import '../../../results/results.dart';
/// Converts tables and types between `drift_dev` internal reprensentation and
/// the one used by the `sqlparser` package.
class TypeMapping {
final DriftOptions options;
final DriftAnalysisDriver driver;
TypeMapping(this.options);
TypeMapping(this.driver);
SqlEngine newEngineWithTables(Iterable<DriftElement> references) {
final engine = driver.newSqlEngine();
for (final reference in references) {
if (reference is DriftTable) {
engine.registerTable(driver.typeMapping.asSqlParserTable(reference));
} else if (reference is DriftView) {
engine.registerView(driver.typeMapping.asSqlParserView(reference));
}
}
return engine;
}
Table asSqlParserTable(DriftTable table) {
return Table(
@ -61,7 +75,7 @@ class TypeMapping {
type: BasicType.int, hint: overrideHint ?? const IsBoolean());
case DriftSqlType.dateTime:
return ResolvedType(
type: options.storeDateTimeValuesAsText
type: driver.options.storeDateTimeValuesAsText
? BasicType.text
: BasicType.int,
hint: overrideHint ?? const IsDateTime(),
@ -85,7 +99,7 @@ class TypeMapping {
case BasicType.int:
if (type.hint is IsBoolean) {
return DriftSqlType.bool;
} else if (!options.storeDateTimeValuesAsText &&
} else if (!driver.options.storeDateTimeValuesAsText &&
type.hint is IsDateTime) {
return DriftSqlType.dateTime;
} else if (type.hint is IsBigInt) {
@ -95,7 +109,8 @@ class TypeMapping {
case BasicType.real:
return DriftSqlType.double;
case BasicType.text:
if (options.storeDateTimeValuesAsText && type.hint is IsDateTime) {
if (driver.options.storeDateTimeValuesAsText &&
type.hint is IsDateTime) {
return DriftSqlType.dateTime;
}

View File

@ -22,7 +22,7 @@ class DriftTriggerResolver
final context = engine.analyzeNode(stmt, source);
reportLints(context);
TriggerTableWrite? mapWrite(TableWrite parserWrite) {
WrittenDriftTable? mapWrite(TableWrite parserWrite) {
drift.UpdateKind kind;
switch (parserWrite.kind) {
case UpdateKind.insert:
@ -40,7 +40,7 @@ class DriftTriggerResolver
.whereType<DriftTable>()
.firstWhereOrNull((e) => e.schemaName == parserWrite.table.name);
if (table != null) {
return TriggerTableWrite(table, kind);
return WrittenDriftTable(table, kind);
} else {
return null;
}
@ -53,7 +53,7 @@ class DriftTriggerResolver
createStmt: source.substring(stmt.firstPosition, stmt.lastPosition),
writes: findWrittenTables(stmt)
.map(mapWrite)
.whereType<TriggerTableWrite>()
.whereType<WrittenDriftTable>()
.toList(),
);
}

View File

@ -0,0 +1,133 @@
import 'package:sqlparser/sqlparser.dart';
import '../driver/driver.dart';
import '../driver/error.dart';
import '../driver/state.dart';
import '../results/file_results.dart';
import '../results/results.dart';
import 'queries/query_analyzer.dart';
import 'queries/required_variables.dart';
class FileAnalyzer {
final DriftAnalysisDriver driver;
FileAnalyzer(this.driver);
Future<FileAnalysisResult> runAnalysisOn(FileState state) async {
final result = FileAnalysisResult();
if (state.extension == '.dart') {
for (final elementAnalysis in state.analysis.values) {
final element = elementAnalysis.result;
final queries = <String, SqlQuery>{};
if (element is BaseDriftAccessor) {
for (final query in element.declaredQueries) {
final engine =
driver.typeMapping.newEngineWithTables(element.references);
final context = engine.analyze(query.sql);
final analyzer = QueryAnalyzer(context, driver,
references: element.references.toList());
queries[query.name] = analyzer.analyze(query);
for (final error in analyzer.lints) {
result.analysisErrors.add(DriftAnalysisError(
error.span, 'Error in ${query.name}: ${error.message}'));
}
}
}
}
} else if (state.extension == '.drift' || state.extension == '.moor') {
// We need to map defined query elements to proper analysis results.
final genericEngineForParsing = driver.newSqlEngine();
final source = await driver.backend.readAsString(state.ownUri);
final parsedFile =
genericEngineForParsing.parse(source).rootNode as DriftFile;
for (final elementAnalysis in state.analysis.values) {
final element = elementAnalysis.result;
if (element is DefinedSqlQuery) {
final engine =
driver.typeMapping.newEngineWithTables(element.references);
final stmt = parsedFile.statements
.firstWhere((e) => e.firstPosition == element.sqlOffset)
as DeclaredStatement;
final options = _createOptionsAndVars(engine, stmt);
final analysisResult =
engine.analyzeNode(stmt, source, stmtOptions: options.options);
final analyzer = QueryAnalyzer(analysisResult, driver,
references: element.references,
requiredVariables: options.variables);
result.resolvedQueries[element.id] = analyzer.analyze(element)
..declaredInDriftFile = true;
for (final error in analyzer.lints) {
result.analysisErrors
.add(DriftAnalysisError(error.span, error.message ?? ''));
}
}
}
}
return result;
}
_OptionsAndRequiredVariables _createOptionsAndVars(
SqlEngine engine, DeclaredStatement stmt) {
final reader = engine.schemaReader;
final indexedHints = <int, ResolvedType>{};
final namedHints = <String, ResolvedType>{};
final defaultValues = <String, Expression>{};
final requiredIndex = <int>{};
final requiredName = <String>{};
for (final parameter in stmt.parameters) {
if (parameter is VariableTypeHint) {
final variable = parameter.variable;
if (parameter.isRequired) {
if (variable is ColonNamedVariable) {
requiredName.add(variable.name);
} else if (variable is NumberedVariable) {
requiredIndex.add(variable.resolvedIndex!);
}
}
if (parameter.typeName != null) {
final type = reader
.resolveColumnType(parameter.typeName)
.withNullable(parameter.orNull);
if (variable is ColonNamedVariable) {
namedHints[variable.name] = type;
} else if (variable is NumberedVariable) {
indexedHints[variable.resolvedIndex!] = type;
}
}
} else if (parameter is DartPlaceholderDefaultValue) {
defaultValues[parameter.variableName] = parameter.defaultValue;
}
}
return _OptionsAndRequiredVariables(
AnalyzeStatementOptions(
indexedVariableTypes: indexedHints,
namedVariableTypes: namedHints,
defaultValuesForPlaceholder: defaultValues,
),
RequiredVariables(requiredIndex, requiredName),
);
}
}
class _OptionsAndRequiredVariables {
final AnalyzeStatementOptions options;
final RequiredVariables variables;
_OptionsAndRequiredVariables(this.options, this.variables);
}

View File

@ -0,0 +1,189 @@
// ignore_for_file: library_private_types_in_public_api
import 'package:sqlparser/sqlparser.dart';
import '../../results/results.dart';
/// Analysis support for nested queries.
///
/// In drift, nested queries can be added with the `LIST` pseudofunction used
/// as a column. At runtime, the nested query is executed and its result are
/// collected as a list used as result for the main query.
/// As an example, consider the following query which selects all friends for
/// all users in a hypothetical social network:
///
/// ```sql
/// SELECT u.**, LIST(SELECT friend.* FROM users friend
/// INNER JOIN friendships f ON f.user_a = friend.id OR f.user_b = friend.id
/// INNER JOIN users other
/// ON other.id IN (f.user_a, f.user_b) AND other.id != friend.id
/// WHERE other.id = u.id) friends
/// FROM users u
/// ```
///
/// This would generate a class with a `User u` and a `List<User> friends;`
/// fields.
///
/// As shown in the example, nested queries can refer to columns from outer
/// queries (here, `WHERE other.id = u.id` refers to `u.id` from the outer
/// query). To implement this with separate runtime queries, a transformation
/// is needed. First, we mark all [Reference]s that capture a value from an
/// outer query. The outer query is then modified to include this reference in
/// its result set. In the inner query, the variable is replaced with a
/// variable. In generated code, we first run the outer query and, for each
/// result, then set the variable and run the inner query.
/// In the example, the two transformed queries could look like this:
///
/// ```
/// a: SELECT u.**, u.id AS "helper0" FROM users u;
/// b: SELECT friend.* FROM users friend
/// ...
/// WHERE other.id = ?;
/// ```
///
/// At runtime, we'd first run `a` and then run `b` with `?` instantiated to
/// `helper0` for each row in `a`.
///
/// When a nested query appears outside of a [NestedQueriesContainer], an
/// error will be reported.
class NestedQueryAnalyzer extends RecursiveVisitor<_AnalyzerState, void> {
int _capturingVariableCounter = 0;
final List<AnalysisError> errors = [];
NestedQueriesContainer analyzeRoot(SelectStatement node) {
final container = NestedQueriesContainer(node);
final state = _AnalyzerState(container);
node.accept(this, state);
state._process();
return container;
}
@override
void visitDriftSpecificNode(DriftSpecificNode e, _AnalyzerState arg) {
if (e is NestedQueryColumn) {
final expectedParent = arg.container.select;
if (e.parent != expectedParent || !expectedParent.columns.contains(e)) {
// Not in a valid container or placed in an illegal position - report
// error!
errors.add(AnalysisError(
relevantNode: e,
message: 'A `LIST` result cannot be used here!',
type: AnalysisErrorType.other,
));
}
final nested = NestedQuery(arg.container, e);
arg.container.nestedQueries[e] = nested;
final childState = _AnalyzerState(nested);
super.visitDriftSpecificNode(e, childState);
childState._process();
return;
}
super.visitDriftSpecificNode(e, arg);
}
@override
void visitVariable(Variable e, _AnalyzerState arg) {
arg.actualAndAddedVariables.add(e);
super.visitVariable(e, arg);
}
@override
void visitReference(Reference e, _AnalyzerState arg) {
final resultEntity = e.resultEntity;
final container = arg.container;
if (resultEntity != null && container is NestedQuery) {
if (!resultEntity.origin.isChildOf(arg.container.select)) {
// Reference captures a variable outside of this query
final capture = container.capturedVariables[e] =
CapturedVariable(e, _capturingVariableCounter++);
// Keep track of the position of the variable so that we can later
// assign it the right index.
capture.introducedVariable.setSpan(e.first!, e.last!);
arg.actualAndAddedVariables.add(capture.introducedVariable);
}
} else {
// todo: Reference not resolved properly. An error should have been
// reported already, but we'll definitely not generate correct code for
// this.
}
}
}
class _AnalyzerState {
final NestedQueriesContainer container;
final List<Variable> actualAndAddedVariables = [];
_AnalyzerState(this.container);
void _process() {
// Add necessary columns to select variables read by inner nested queries.
for (final variable in container.variablesCapturedByChildren) {
container.addedColumns.add(
ExpressionResultColumn(
expression: Reference(
entityName: variable.reference.entityName,
columnName: variable.reference.columnName,
),
as: variable.helperColumn,
),
);
}
// Re-index variables, this time also considering the synthetic variables
// that we'll insert in [addHelperNodes] later.
AstPreparingVisitor.resolveIndexOfVariables(actualAndAddedVariables);
}
}
/// Rewrites the query backing the [rootContainer] to
///
/// - add result columns for outgoing references in nested queries
/// - replace outgoing references with variables
SelectStatement addHelperNodes(NestedQueriesContainer rootContainer) {
return _NestedQueryTransformer()
.transform(rootContainer.select, rootContainer) as SelectStatement;
}
class _NestedQueryTransformer extends Transformer<NestedQueriesContainer> {
@override
AstNode? visitSelectStatement(SelectStatement e, NestedQueriesContainer arg) {
if (e == arg.select) {
for (final column in arg.addedColumns) {
e.columns.add(column..parent = e);
}
}
return super.visitSelectStatement(e, arg);
}
@override
AstNode? visitDriftSpecificNode(
DriftSpecificNode e, NestedQueriesContainer arg) {
if (e is NestedQueryColumn) {
final child = arg.nestedQueries[e];
if (child != null) {
e.transformChildren(this, child);
}
// Remove nested query colums from the parent query
return null;
}
return super.visitDriftSpecificNode(e, arg);
}
@override
AstNode? visitReference(Reference e, NestedQueriesContainer arg) {
final captured = arg is NestedQuery ? arg.capturedVariables[e] : null;
if (captured != null) {
return captured.introducedVariable;
}
return super.visitReference(e, arg);
}
}

View File

@ -0,0 +1,693 @@
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/utils/find_referenced_tables.dart';
import '../../driver/driver.dart';
import '../drift/sqlparser/drift_lints.dart';
import '../drift/sqlparser/mapping.dart';
import '../../results/results.dart';
import 'nested_queries.dart';
import 'required_variables.dart';
/// The context contains all data that is required to create an [SqlQuery]. This
/// class is simply there to bundle the data.
class _QueryHandlerContext {
final List<FoundElement> foundElements;
final AstNode root;
final NestedQueriesContainer? nestedScope;
final String queryName;
final String? requestedResultClass;
_QueryHandlerContext({
required List<FoundElement> foundElements,
required this.root,
required this.queryName,
required this.nestedScope,
this.requestedResultClass,
}) : foundElements = List.unmodifiable(foundElements);
}
/// Maps an [AnalysisContext] from the sqlparser to a [SqlQuery] from this
/// generator package by determining its type, return columns, variables and so
/// on.
class QueryAnalyzer {
final AnalysisContext context;
final DriftAnalysisDriver driver;
final RequiredVariables requiredVariables;
final Map<String, DriftElement> referencesByName;
/// Found tables and views found need to be shared between the query and
/// all subqueries to not muss any updates when watching query.
late Set<Table> _foundTables;
late Set<View> _foundViews;
/// Used to create a unique name for every nested query. This needs to be
/// shared between queries, therefore this should not be part of the
/// context.
int nestedQueryCounter = 0;
final List<AnalysisError> lints = [];
QueryAnalyzer(
this.context,
this.driver, {
required List<DriftElement> references,
this.requiredVariables = RequiredVariables.empty,
}) : referencesByName = {
for (final reference in references)
reference.id.name.toLowerCase(): reference,
};
E _lookupReference<E extends DriftElement?>(String name) {
return referencesByName[name.toLowerCase()] as E;
}
SqlQuery analyze(DriftQueryDeclaration declaration) {
final nestedAnalyzer = NestedQueryAnalyzer();
NestedQueriesContainer? nestedScope;
if (context.root is SelectStatement) {
nestedScope = nestedAnalyzer.analyzeRoot(context.root as SelectStatement);
}
final foundElements = _extractElements(
ctx: context,
root: context.root,
required: requiredVariables,
nestedScope: nestedScope,
);
_verifyNoSkippedIndexes(foundElements);
final String? requestedResultClass;
if (declaration is DefinedSqlQuery) {
requestedResultClass = declaration.resultClassName;
} else {
requestedResultClass = null;
}
final query = _mapToDrift(_QueryHandlerContext(
foundElements: foundElements,
queryName: declaration.name,
requestedResultClass: requestedResultClass,
root: context.root,
nestedScope: nestedScope,
));
final linter = DriftSqlLinter(context);
linter.collectLints();
lints
..addAll(context.errors)
..addAll(linter.sqlParserErrors)
..addAll(nestedAnalyzer.errors);
return query;
}
SqlQuery _mapToDrift(_QueryHandlerContext queryContext) {
if (queryContext.root is BaseSelectStatement) {
return _handleSelect(queryContext);
} else if (queryContext.root is UpdateStatement ||
queryContext.root is DeleteStatement ||
queryContext.root is InsertStatement) {
return _handleUpdate(queryContext);
} else {
throw StateError(
'Unexpected sql: Got ${queryContext.root}, expected insert, select, '
'update or delete');
}
}
void _applyFoundTables(ReferencedTablesVisitor visitor) {
_foundTables = visitor.foundTables;
_foundViews = visitor.foundViews;
}
UpdatingQuery _handleUpdate(_QueryHandlerContext queryContext) {
final root = queryContext.root;
final updatedFinder = UpdatedTablesVisitor();
root.acceptWithoutArg(updatedFinder);
_applyFoundTables(updatedFinder);
final isInsert = root is InsertStatement;
InferredResultSet? resultSet;
if (root is StatementReturningColumns) {
final columns = root.returnedResultSet?.resolvedColumns;
if (columns != null) {
resultSet = _inferResultSet(queryContext, columns);
}
}
return UpdatingQuery(
queryContext.queryName,
context,
root,
queryContext.foundElements,
updatedFinder.writtenTables
.map((write) {
final table = _lookupReference<DriftTable?>(write.table.name);
drift.UpdateKind kind;
switch (write.kind) {
case UpdateKind.insert:
kind = drift.UpdateKind.insert;
break;
case UpdateKind.update:
kind = drift.UpdateKind.update;
break;
case UpdateKind.delete:
kind = drift.UpdateKind.delete;
break;
}
return table != null ? WrittenDriftTable(table, kind) : null;
})
.whereType<WrittenDriftTable>()
.toList(),
isInsert: isInsert,
hasMultipleTables: updatedFinder.foundTables.length > 1,
resultSet: resultSet,
);
}
SqlSelectQuery _handleSelect(_QueryHandlerContext queryContext) {
final tableFinder = ReferencedTablesVisitor();
queryContext.root.acceptWithoutArg(tableFinder);
_applyFoundTables(tableFinder);
final driftTables = _foundTables
.map((tbl) => _lookupReference<DriftTable?>(tbl.name))
.whereType<DriftTable>()
.toList();
final driftViews = _foundViews
.map((tbl) => _lookupReference<DriftView?>(tbl.name))
.whereType<DriftView>()
.toList();
final driftEntities = [...driftTables, ...driftViews];
return SqlSelectQuery(
queryContext.queryName,
context,
queryContext.root,
queryContext.foundElements,
driftEntities,
_inferResultSet(
queryContext,
(queryContext.root as BaseSelectStatement).resolvedColumns!,
),
queryContext.requestedResultClass,
queryContext.nestedScope,
);
}
InferredResultSet _inferResultSet(
_QueryHandlerContext queryContext,
List<Column> rawColumns,
) {
final candidatesForSingleTable = {..._foundTables, ..._foundViews};
final columns = <ResultColumn>[];
// First, go through regular result columns
for (final column in rawColumns) {
final type = context.typeOf(column).type;
final driftType = driver.typeMapping.sqlTypeToDrift(type);
AppliedTypeConverter? converter;
if (type?.hint is TypeConverterHint) {
converter = (type!.hint as TypeConverterHint).converter;
}
columns.add(ResultColumn(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();
}
// 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 =
_lookupReference<DriftElementWithResultSet?>(table.name);
if (driftTable == null) {
// References a table not declared in any drift api (dart or drift file).
// This can happen for internal sqlite tables
return InferredResultSet(null, columns);
}
final resultEntryToColumn = <ResultColumn, String>{};
final resultColumnNameToDrift = <String, DriftColumn>{};
var matches = true;
// go trough all columns of the table in question
for (final column in driftTable.columns) {
// check if this column from the table is present in the result set
final tableColumn = table.findColumn(column.nameInSql);
final inResultSet =
rawColumns.where((t) => _toTableOrViewColumn(t) == tableColumn);
if (inResultSet.length == 1) {
// 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];
resultEntryToColumn[resultColumn] = column.nameInDart;
resultColumnNameToDrift[resultColumn.name] = column;
} else {
// it's not, so no match
matches = false;
break;
}
}
// we have established that all columns in resultEntryToColumn do appear
// in the drift table. Now check for set equality.
if (rawColumns.length != driftTable.columns.length) {
matches = false;
}
if (matches) {
final match = MatchingDriftTable(driftTable, resultColumnNameToDrift);
return InferredResultSet(match, columns)
..forceDartNames(resultEntryToColumn);
}
}
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,
)),
));
}
}
return nestedTables;
}
Column? _toTableOrViewColumn(Column? c) {
// ignore: literal_only_boolean_expressions
while (true) {
if (c is TableColumn || c is ViewColumn) {
return c;
} else if (c is ExpressionColumn) {
final expression = c.expression;
if (expression is Reference) {
final resolved = expression.resolved;
if (resolved is Column) {
c = resolved;
continue;
}
}
// Not a reference to a column
return null;
} else if (c is DelegatedColumn) {
c = c.innerColumn;
} else {
return null;
}
}
}
ResultSet? _resultSetOfColumn(Column c) {
final mapped = _toTableOrViewColumn(c);
if (mapped == null) return null;
if (mapped is ViewColumn) {
return mapped.view;
} else {
return (mapped as TableColumn).table;
}
}
/// Extracts variables and Dart templates from the AST tree starting at
/// [root], but nested queries are excluded. Variables are sorted by their
/// ascending index. Placeholders are sorted by the position they have in the
/// query. When comparing variables and placeholders, the variable comes first
/// if the first variable with the same index appears before the placeholder.
///
/// Additionally, the following assumptions can be made if this method returns
/// without throwing:
/// - array variables don't have an explicit index
/// - if an explicitly indexed variable appears AFTER an array variable or
/// a Dart placeholder, its indexed is LOWER than that element. This means
/// that elements can be expanded into multiple variables without breaking
/// variables that appear after them.
List<FoundElement> _extractElements({
required AnalysisContext ctx,
required AstNode root,
NestedQueriesContainer? nestedScope,
RequiredVariables required = RequiredVariables.empty,
}) {
final collector = _FindElements()..visit(root, nestedScope);
// this contains variable references. For instance, SELECT :a = :a would
// contain two entries, both referring to the same variable. To do that,
// we use the fact that each variable has a unique index.
final variables = collector.variables;
final placeholders = collector.dartPlaceholders;
final merged = _mergeVarsAndPlaceholders(variables, placeholders);
final foundElements = <FoundElement>[];
// we don't allow variables with an explicit index after an array. For
// instance: SELECT * FROM t WHERE id IN ? OR id = ?2. The reason this is
// not allowed is that we expand the first arg into multiple vars at runtime
// which would break the index. The initial high values can be arbitrary.
// We've chosen 999 because most sqlite binaries don't allow more variables.
var maxIndex = 999;
var currentIndex = 0;
for (final used in merged) {
if (used is Variable) {
if (used.resolvedIndex == currentIndex) {
continue; // already handled, we only report a single variable / index
}
currentIndex = used.resolvedIndex!;
final name = (used is ColonNamedVariable) ? used.name : null;
final explicitIndex =
(used is NumberedVariable) ? used.explicitIndex : null;
final forCapture = used.meta<CapturedVariable>();
final internalType =
// If this variable was introduced to replace a reference from a
// `LIST` query to an outer query, use the type of the reference
// instead of the synthetic variable that we're replacing it with.
ctx.typeOf(forCapture != null ? forCapture.reference : used);
final type = driver.typeMapping.sqlTypeToDrift(internalType.type);
if (forCapture != null) {
foundElements.add(FoundVariable.nestedQuery(
index: currentIndex,
name: name,
sqlType: type,
variable: used,
forCaptured: forCapture,
));
continue;
}
final isArray = internalType.type?.isArray ?? false;
final isRequired = required.requiredNamedVariables.contains(name) ||
required.requiredNumberedVariables.contains(used.resolvedIndex);
if (explicitIndex != null && currentIndex >= maxIndex) {
throw ArgumentError(
'Cannot have a variable with an index lower than that of an '
'array appearing after an array!');
}
AppliedTypeConverter? converter;
// Recognizing type converters on variables is opt-in since it would
// break existing code.
if (driver.options.applyConvertersOnVariables &&
internalType.type?.hint is TypeConverterHint) {
converter = (internalType.type!.hint as TypeConverterHint).converter;
}
foundElements.add(FoundVariable(
index: currentIndex,
name: name,
sqlType: type,
nullable: internalType.type?.nullable ?? false,
variable: used,
isArray: isArray,
typeConverter: converter,
isRequired: isRequired,
));
// arrays cannot be indexed explicitly because they're expanded into
// multiple variables when executed
if (isArray && explicitIndex != null) {
throw ArgumentError(
'Cannot use an array variable with an explicit index');
}
if (isArray) {
maxIndex = used.resolvedIndex!;
}
} else if (used is DartPlaceholder) {
// we don't what index this placeholder has, so we can't allow _any_
// explicitly indexed variables coming after this
maxIndex = 0;
foundElements.add(_extractPlaceholder(ctx, used));
}
}
return foundElements;
}
FoundDartPlaceholder _extractPlaceholder(
AnalysisContext context, DartPlaceholder placeholder) {
final name = placeholder.name;
final type = placeholder.when(
isExpression: (e) {
final foundType = context.typeOf(e);
DriftSqlType? columnType;
if (foundType.type != null) {
columnType = driver.typeMapping.sqlTypeToDrift(foundType.type);
}
final defaultValue =
context.stmtOptions.defaultValuesForPlaceholder[name];
return ExpressionDartPlaceholderType(columnType, defaultValue);
},
isLimit: (_) =>
SimpleDartPlaceholderType(SimpleDartPlaceholderKind.limit),
isOrderBy: (_) =>
SimpleDartPlaceholderType(SimpleDartPlaceholderKind.orderBy),
isOrderingTerm: (_) =>
SimpleDartPlaceholderType(SimpleDartPlaceholderKind.orderByTerm),
isInsertable: (_) {
final insert = placeholder.parents.whereType<InsertStatement>().first;
final table = insert.table.resultSet;
return InsertableDartPlaceholderType(
table is Table ? _lookupReference(table.name) as DriftTable : null);
},
);
final availableResults = placeholder.statementScope.allAvailableResultSets;
final availableDriftResults = <AvailableDriftResultSet>[];
for (final available in availableResults) {
final aliasedResultSet = available.resultSet.resultSet;
final resultSet = aliasedResultSet?.unalias();
String name;
if (aliasedResultSet is NamedResultSet) {
name = aliasedResultSet.name;
} else {
// If we don't have a name we can't include this result set.
continue;
}
DriftElementWithResultSet driftEntity;
if (resultSet is Table || resultSet is View) {
driftEntity = _lookupReference((resultSet as NamedResultSet).name)
as DriftElementWithResultSet;
} else {
// If this result set is an inner select statement or anything else we
// can't represent it in Dart.
continue;
}
availableDriftResults
.add(AvailableDriftResultSet(name, driftEntity, available));
}
return FoundDartPlaceholder(type!, name, availableDriftResults)
..astNode = placeholder;
}
/// Merges [vars] and [placeholders] into a list that satisfies the order
/// described in [_extractElements].
List<dynamic /* Variable|DartPlaceholder */ > _mergeVarsAndPlaceholders(
List<Variable> vars, List<DartPlaceholder> placeholders) {
final groupVarsByIndex = <int, List<Variable>>{};
for (final variable in vars) {
groupVarsByIndex
.putIfAbsent(variable.resolvedIndex!, () => [])
.add(variable);
}
// sort each group by index
for (final group in groupVarsByIndex.values) {
group.sort((a, b) => a.resolvedIndex!.compareTo(b.resolvedIndex!));
}
late int Function(dynamic, dynamic) comparer;
comparer = (dynamic a, dynamic b) {
if (a is Variable && b is Variable) {
// variables are sorted by their index
return a.resolvedIndex!.compareTo(b.resolvedIndex!);
} else if (a is DartPlaceholder && b is DartPlaceholder) {
// placeholders by their position
return AnalysisContext.compareNodesByOrder(a, b);
} else {
// ok, one of them is a variable, the other one is a placeholder. Let's
// assume a is the variable. If not, we just switch results.
if (a is Variable) {
final placeholderB = b as DartPlaceholder;
final firstWithSameIndex = groupVarsByIndex[a.resolvedIndex]!.first;
return firstWithSameIndex.firstPosition
.compareTo(placeholderB.firstPosition);
} else {
return -comparer(b, a);
}
}
};
final list = vars.cast<dynamic>().followedBy(placeholders).toList();
return list..sort(comparer);
}
/// We verify that no variable numbers are skipped in the query. For instance,
/// `SELECT * FROM tbl WHERE a = ?2 AND b = ?` would fail this check because
/// the index 1 is never used.
void _verifyNoSkippedIndexes(List<FoundElement> foundElements) {
final variables = List.of(foundElements.whereType<FoundVariable>())
..sort((a, b) => a.index.compareTo(b.index));
var currentExpectedIndex = 1;
for (var i = 0; i < variables.length; i++) {
final current = variables[i];
if (current.index > currentExpectedIndex) {
throw StateError('This query skips some variable indexes: '
'We found no variable is at position $currentExpectedIndex, '
'even though a variable at index ${current.index} exists.');
}
if (i < variables.length - 1) {
final next = variables[i + 1];
if (next.index > currentExpectedIndex) {
// if the next variable has a higher index, increment expected index
// by one because we expect that every index is present
currentExpectedIndex++;
}
}
}
}
}
/// Finds variables, Dart placeholders and outgoing references from nested
/// queries (which are eventually turned into variables) inside a query.
///
/// Nested children of this query are ignored, see `nested_queries.dart` for
/// details on nested queries and how they're implemented.
class _FindElements extends RecursiveVisitor<NestedQueriesContainer?, void> {
final List<Variable> variables = [];
final List<DartPlaceholder> dartPlaceholders = [];
@override
void visitVariable(Variable e, NestedQueriesContainer? arg) {
variables.add(e);
super.visitVariable(e, arg);
}
@override
void visitDriftSpecificNode(
DriftSpecificNode e, NestedQueriesContainer? arg) {
if (e is NestedQueryColumn) {
// If the node ist a nested query, return to avoid collecting elements
// inside of it
return;
}
if (e is DartPlaceholder) {
dartPlaceholders.add(e);
}
super.visitDriftSpecificNode(e, arg);
}
@override
void visitReference(Reference e, NestedQueriesContainer? arg) {
if (arg is NestedQuery) {
final captured = arg.capturedVariables[e];
if (captured != null) {
variables.add(captured.introducedVariable);
}
}
super.visitReference(e, arg);
}
}

View File

@ -0,0 +1,9 @@
class RequiredVariables {
final Set<int> requiredNumberedVariables;
final Set<String> requiredNamedVariables;
const RequiredVariables(
this.requiredNumberedVariables, this.requiredNamedVariables);
static const RequiredVariables empty = RequiredVariables({}, {});
}

View File

@ -91,7 +91,8 @@ class DatabaseAccessor extends BaseDriftAccessor {
/// analysis happens during code generation because intermediate state is hard
/// to serialize and there are little benefits of analyzing queries early.
@JsonSerializable()
class QueryOnAccessor {
class QueryOnAccessor implements DriftQueryDeclaration {
@override
final String name;
final String sql;

View File

@ -0,0 +1,18 @@
import '../driver/error.dart';
import '../driver/state.dart';
import 'element.dart';
import 'query.dart';
class FileAnalysisResult {
final List<DriftAnalysisError> analysisErrors = [];
final Map<DriftElementId, SqlQuery> resolvedQueries = {};
final Map<DriftElementId, ResolvedDatabaseAccessor> resolvedDatabases = {};
}
class ResolvedDatabaseAccessor {
final Map<String, SqlQuery> definedQueries;
final List<FileState> knownImports;
ResolvedDatabaseAccessor(this.definedQueries, this.knownImports);
}

View File

@ -1,4 +1,20 @@
import 'package:collection/collection.dart';
import 'package:drift/drift.dart' show DriftSqlType, UpdateKind;
import 'package:recase/recase.dart';
import 'package:sqlparser/sqlparser.dart';
import '../../analyzer/options.dart';
import '../resolver/shared/column_name.dart';
import 'column.dart';
import 'element.dart';
import 'result_sets.dart';
import 'table.dart';
import 'types.dart';
import 'view.dart';
abstract class DriftQueryDeclaration {
String get name;
}
/// A named SQL query defined in a `.drift` file. A later compile step will
/// further analyze this query and run analysis on it.
@ -11,10 +27,12 @@ import 'element.dart';
/// since they are local elements which can't be referenced by others, there's
/// no clear advantage wrt. incremental compilation if queries are fully
/// analyzed and serialized. So, we just do this in the generator.
class DefinedSqlQuery extends DriftElement {
class DefinedSqlQuery extends DriftElement implements DriftQueryDeclaration {
/// The unmodified source of the declared SQL statement forming this query.
final String sql;
final String? resultClassName;
/// The offset of [sql] in the source file, used to properly report errors
/// later.
final int sqlOffset;
@ -22,11 +40,823 @@ class DefinedSqlQuery extends DriftElement {
@override
final List<DriftElement> references;
@override
String get name => id.name;
DefinedSqlQuery(
super.id,
super.declaration, {
required this.references,
required this.sql,
required this.sqlOffset,
this.resultClassName,
});
}
/// A fully-resolved and analyzed SQL query.
abstract class SqlQuery {
final String name;
AnalysisContext? get fromContext;
AstNode? get root;
/// Whether this query was declared in a `.drift` file.
///
/// At the moment, there is not much of a difference between drift-defined
/// queries and those defined on a database annotation.
/// However, with a legacy build option, additional `get` and `watch` method
/// are generated for Dart queries whereas drift-defined queries will only
/// generate a single method returning a `Selectable`.
bool declaredInDriftFile = false;
String? get sql => fromContext?.sql;
/// The result set of this statement, mapped to drift-generated classes.
///
/// This is non-nullable for select queries. Updating queries might have a
/// result set if they have a `RETURNING` clause.
InferredResultSet? get resultSet;
/// The variables that appear in the [sql] query. We support three kinds of
/// sql variables: The regular "?" variables, explicitly indexed "?xyz"
/// variables and colon-named variables. Even though this feature is not
/// supported by sqlite directly, we provide syntax sugar for expressions like
/// `column IN ?`, where the variable will have a [List] type at runtime and
/// expand to the appropriate tuple (e.g. `column IN (?, ?, ?)` when the
/// variable is bound to a list with three elements). To make the desugaring
/// easier at runtime, we require that:
///
/// 1. Array arguments don't have an explicit index (`<expr> IN ?1` is
/// forbidden). The reason is that arrays get expanded to multiple
/// variables at runtime, so setting an explicit index doesn't make sense.
/// 2. We only allow explicitly-indexed variables to appear after an array
/// if their index is lower than that of the array (e.g `a = ?2 AND b IN ?
/// AND c IN ?1`. In other words, we can expand an array without worrying
/// about the variables that appear after that array.
late List<FoundVariable> variables;
/// The placeholders in this query which are bound and converted to sql at
/// runtime. For instance, in `SELECT * FROM tbl WHERE $expr`, the `expr` is
/// going to be a [FoundDartPlaceholder] with the type
/// [ExpressionDartPlaceholderType] and [DriftSqlType.bool]. We will
/// generate a method which has a `Expression<bool, BoolType> expr` parameter.
late List<FoundDartPlaceholder> placeholders;
/// Union of [variables] and [placeholders], but in the order in which they
/// appear inside the query.
final List<FoundElement> elements;
/// Whether the underlying sql statement of this query operates on more than
/// one table. In that case, column references in Dart placeholders have to
/// write their table name (e.g. `foo.bar` instead of just `bar`).
final bool hasMultipleTables;
SqlQuery(this.name, this.elements, {bool? hasMultipleTables})
: hasMultipleTables = hasMultipleTables ?? false {
variables = elements.whereType<FoundVariable>().toList();
placeholders = elements.whereType<FoundDartPlaceholder>().toList();
}
bool get needsAsyncMapping {
final result = resultSet;
if (result != null) {
// Mapping to tables is asynchronous
if (result.matchingTable != null) return true;
if (result.nestedResults.any((e) => e is NestedResultTable)) return true;
}
return false;
}
String get resultClassName {
final resultSet = this.resultSet;
if (resultSet == null) {
throw StateError('This query ($name) does not have a result set');
}
if (resultSet.matchingTable != null || resultSet.singleColumn) {
throw UnsupportedError('This result set does not introduce a class, '
'either because it has a matching table or because it only returns '
'one column.');
}
return resultSet.resultClassName ?? '${ReCase(name).pascalCase}Result';
}
/// Returns all found elements, from this query an all nested queries. The
/// elements returned by this method are in no particular order, thus they
/// can only be used to determine the method parameters.
///
/// This method makes some effort to remove duplicated parameters. But only
/// by comparing the dart name.
List<FoundElement> elementsWithNestedQueries() {
final elements = List.of(this.elements);
final subQueries = resultSet?.nestedResults.whereType<NestedResultQuery>();
for (final subQuery in subQueries ?? const <NestedResultQuery>[]) {
for (final subElement in subQuery.query.elementsWithNestedQueries()) {
if (elements
.none((e) => e.dartParameterName == subElement.dartParameterName)) {
elements.add(subElement);
}
}
}
return elements;
}
}
class SqlSelectQuery extends SqlQuery {
final List<DriftElement> readsFrom;
@override
final InferredResultSet resultSet;
@override
final AnalysisContext fromContext;
@override
final AstNode root;
/// The name of the result class, as requested by the user.
// todo: Allow custom result classes for RETURNING as well?
final String? requestedResultClass;
final NestedQueriesContainer? nestedContainer;
/// Whether this query contains nested queries or not
bool get hasNestedQuery =>
resultSet.nestedResults.any((e) => e is NestedResultQuery);
@override
bool get needsAsyncMapping => hasNestedQuery || super.needsAsyncMapping;
SqlSelectQuery(
String name,
this.fromContext,
this.root,
List<FoundElement> elements,
this.readsFrom,
this.resultSet,
this.requestedResultClass,
this.nestedContainer,
) : super(name, elements, hasMultipleTables: readsFrom.length > 1);
Set<DriftTable> get readsFromTables {
return {
for (final entity in readsFrom)
if (entity is DriftTable)
entity
else if (entity is DriftView)
...entity.transitiveTableReferences,
};
}
/// Creates a copy of this [SqlSelectQuery] with a new [resultSet].
///
/// The copy won't have a [requestedResultClass].
SqlSelectQuery replaceResultSet(InferredResultSet resultSet) {
return SqlSelectQuery(
name,
fromContext,
root,
elements,
readsFrom,
resultSet,
null,
nestedContainer,
);
}
}
/// Something that can contain nested queries.
///
/// This contains the root select statement and all nested queries that appear
/// in a nested queries container.
class NestedQueriesContainer {
final SelectStatement select;
final Map<NestedQueryColumn, NestedQuery> nestedQueries = {};
NestedQueriesContainer(this.select);
/// Columns that should be added to the [select] statement to read variables
/// captured by children.
///
/// These columns aren't mounted to the same syntax tree as [select], they
/// will be mounted into the tree returned by [addHelperNodes].
final List<ExpressionResultColumn> addedColumns = [];
Iterable<CapturedVariable> get variablesCapturedByChildren {
return nestedQueries.values
.expand((nested) => nested.capturedVariables.values);
}
}
/// A nested query found in a SQL statement.
///
/// See the `NestedQueryAnalyzer` for an overview on how nested queries work.
class NestedQuery extends NestedQueriesContainer {
final NestedQueryColumn queryColumn;
final NestedQueriesContainer parent;
/// All references that read from a table only available in the outer
/// select statement. It will need to be transformed in a later step.
final Map<Reference, CapturedVariable> capturedVariables = {};
NestedQuery(this.parent, this.queryColumn) : super(queryColumn.select);
}
class CapturedVariable {
final Reference reference;
/// A number uniquely identifying this captured variable in the select
/// statement analyzed.
///
/// This is used to add the necessary helper column later.
final int queryGlobalId;
/// The variable introduced to replace the original reference.
///
/// This variable is not mounted to the same syntax tree as [reference], it
/// will be mounted into the tree returned by [addHelperNodes].
final ColonNamedVariable introducedVariable;
String get helperColumn => '\$n_$queryGlobalId';
CapturedVariable(this.reference, this.queryGlobalId)
: introducedVariable = ColonNamedVariable.synthetic(':r$queryGlobalId') {
introducedVariable.setMeta<CapturedVariable>(this);
}
}
class WrittenDriftTable {
final DriftTable table;
final UpdateKind kind;
WrittenDriftTable(this.table, this.kind);
}
class UpdatingQuery extends SqlQuery {
final List<WrittenDriftTable> updates;
final bool isInsert;
@override
final InferredResultSet? resultSet;
@override
final AnalysisContext fromContext;
@override
final AstNode root;
bool get isOnlyDelete => updates.every((w) => w.kind == UpdateKind.delete);
bool get isOnlyUpdate => updates.every((w) => w.kind == UpdateKind.update);
UpdatingQuery(
String name,
this.fromContext,
this.root,
List<FoundElement> elements,
this.updates, {
this.isInsert = false,
bool? hasMultipleTables,
this.resultSet,
}) : super(name, elements, hasMultipleTables: hasMultipleTables);
}
/// A special kind of query running multiple inner queries in a transaction.
class InTransactionQuery extends SqlQuery {
final List<SqlQuery> innerQueries;
InTransactionQuery(this.innerQueries, String name)
: super(name, [for (final query in innerQueries) ...query.elements]);
@override
InferredResultSet? get resultSet => null;
@override
AnalysisContext? get fromContext => null;
@override
AstNode? get root => null;
}
class InferredResultSet {
/// If the result columns of a SELECT statement exactly match one table, we
/// can just use the data class generated for that table. Otherwise, we'd have
/// 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;
final List<ResultColumn> columns;
final Map<ResultColumn, String> _dartNames = {};
/// The name of the Dart class generated to store this result set, or null if
/// it hasn't explicitly been set.
final String? resultClassName;
/// Explicitly controls that no result class should be generated for this
/// result set.
///
/// This is enabled on duplicate result sets caused by custom result class
/// names.
final bool dontGenerateResultClass;
InferredResultSet(
this.matchingTable,
this.columns, {
this.nestedResults = const [],
this.resultClassName,
this.dontGenerateResultClass = false,
});
/// 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
/// - return an existing table model
/// - return exactly one column
///
/// We always need to generate a class if the query contains nested results.
bool get needsOwnClass {
return matchingTable == null &&
(columns.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;
String? nestedPrefixFor(NestedResult table) {
if (_expandedNestedPrefixes == null) {
var index = 0;
_expandedNestedPrefixes = {
for (final nested in nestedResults) nested: 'nested_${index++}',
};
}
return _expandedNestedPrefixes![table];
}
void forceDartNames(Map<ResultColumn, String> names) {
_dartNames
..clear()
..addAll(names);
}
/// Suggests an appropriate name that can be used as a dart field for the
/// [column].
String dartNameFor(ResultColumn column) {
return _dartNames.putIfAbsent(column, () {
return dartNameForSqlColumn(column.name,
existingNames: _dartNames.values);
});
}
/// [hashCode] that matches [isCompatibleTo] instead of `==`.
int get compatibilityHashCode => Object.hash(
Object.hashAll(columns.map((e) => e.compatibilityHashCode)),
Object.hashAll(nestedResults.map((e) => e.compatibilityHashCode)),
);
/// Checks whether this and the [other] result set have the same columns and
/// 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);
}
}
/// Information about a matching table. A table matches a query if a query
/// selects all columns from that table, and nothing more.
///
/// We still need to handle column aliases.
class MatchingDriftTable {
final DriftElementWithResultSet table;
final Map<String, DriftColumn> aliasToColumn;
MatchingDriftTable(this.table, this.aliasToColumn);
/// Whether the column alias can be ignored.
///
/// This is the case if each result column name maps to a drift column with
/// the same SQL name.
bool get effectivelyNoAlias {
return !aliasToColumn.entries
.any((entry) => entry.key != entry.value.nameInSql);
}
}
class ResultColumn implements HasType {
final String name;
@override
final DriftSqlType sqlType;
@override
final bool nullable;
@override
final AppliedTypeConverter? typeConverter;
/// The analyzed column from the `sqlparser` package.
final Column? sqlParserColumn;
ResultColumn(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);
}
/// 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 &&
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 table extracted from a `**` column.
///
/// For instance, consider this query:
/// ```sql
/// CREATE TABLE groups (id INTEGER NOT NULL PRIMARY KEY);
/// CREATE TABLE users (id INTEGER NOT NULL PRIMARY KEY);
/// CREATE TABLE members (
/// group INT REFERENCES ..,
/// user INT REFERENCES ...,
/// is_admin BOOLEAN
/// );
///
/// membersOf: SELECT users.**, members.is_admin FROM members
/// INNER JOIN users ON users.id = members.user;
/// ```
///
/// The generated result set should now look like this:
/// ```dart
/// class MembersOfResult {
/// final User users;
/// final bool isAdmin;
/// }
/// ```
///
/// Knowing that `User` should be extracted into a field is represented with a
/// [NestedResultTable] information as part of the result set.
class NestedResultTable extends NestedResult {
final bool isNullable;
final NestedStarResultColumn from;
final String name;
final DriftElementWithResultSet table;
NestedResultTable(this.from, this.name, this.table, {this.isNullable = true});
String get dartFieldName => ReCase(name).camelCase;
/// [hashCode] that matches [isCompatibleTo] instead of `==`.
@override
int get compatibilityHashCode {
return Object.hash(name, table);
}
/// 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) {
if (other is! NestedResultTable) return false;
return other.name == name &&
other.table == table &&
other.isNullable == isNullable;
}
}
class NestedResultQuery extends NestedResult {
final NestedQueryColumn from;
final SqlSelectQuery query;
NestedResultQuery({
required this.from,
required this.query,
});
String filedName() {
if (from.as != null) {
return from.as!;
}
return ReCase(query.name).camelCase;
}
// Because it is currently not possible to reuse result classes from queries
// that use nested queries, every instance should be different. Therefore
// the object hashCode and equality operator is just fine.
@override
int get compatibilityHashCode => hashCode;
@override
bool isCompatibleTo(NestedResult other) => this == other;
}
/// Something in the query that needs special attention when generating code,
/// such as variables or Dart placeholders.
abstract class FoundElement {
String get dartParameterName;
/// The name of this element as declared in the query
String? get name;
bool get hasSqlName => name != null;
/// If the element should be hidden from the parameter list
bool get hidden => false;
}
/// A semantic interpretation of a [Variable] in a sql statement.
class FoundVariable extends FoundElement implements HasType {
/// The (unique) index of this variable in the sql query. For instance, the
/// query `SELECT * FROM tbl WHERE a = ? AND b = :xyz OR c = :xyz` contains
/// three [Variable]s in its AST, but only two [FoundVariable]s, where the
/// `?` will have index 1 and (both) `:xyz` variables will have index 2. We
/// only report one [FoundVariable] per index.
int index;
/// The name of this variable, or null if it's not a named variable.
@override
String? name;
/// The (inferred) type for this variable.
@override
final DriftSqlType sqlType;
/// The type converter to apply before writing this value.
@override
final AppliedTypeConverter? typeConverter;
@override
final bool nullable;
/// The first [Variable] in the sql statement that has this [index].
// todo: Do we really need to expose this? We only use [resolvedIndex], which
// should always be equal to [index].
final Variable variable;
/// Whether this variable is an array, which will be expanded into multiple
/// variables at runtime. We only accept queries where no explicitly numbered
/// vars appear after an array. This means that we can expand array variables
/// without having to look at other variables.
@override
final bool isArray;
final bool isRequired;
@override
final bool hidden;
/// When this variable is introduced for a nested query referencing something
/// from an outer query, contains the backing variable.
final CapturedVariable? forCaptured;
FoundVariable({
required this.index,
required this.name,
required this.sqlType,
required this.variable,
this.nullable = false,
this.isArray = false,
this.isRequired = false,
this.typeConverter,
}) : hidden = false,
forCaptured = null,
assert(variable.resolvedIndex == index);
FoundVariable.nestedQuery({
required this.index,
required this.name,
required this.sqlType,
required this.variable,
required this.forCaptured,
}) : typeConverter = null,
nullable = false,
isArray = false,
isRequired = true,
hidden = true;
@override
String get dartParameterName {
if (name != null) {
return dartNameForSqlColumn(name!);
} else {
return 'var${variable.resolvedIndex}';
}
}
}
abstract class DartPlaceholderType {}
enum SimpleDartPlaceholderKind {
limit,
orderByTerm,
orderBy,
}
class SimpleDartPlaceholderType extends DartPlaceholderType {
final SimpleDartPlaceholderKind kind;
SimpleDartPlaceholderType(this.kind);
@override
int get hashCode => kind.hashCode;
@override
bool operator ==(Object other) {
return other is SimpleDartPlaceholderType && other.kind == kind;
}
}
class ExpressionDartPlaceholderType extends DartPlaceholderType {
/// The sql type of this expression.
final DriftSqlType? columnType;
final Expression? defaultValue;
ExpressionDartPlaceholderType(this.columnType, this.defaultValue);
@override
int get hashCode => Object.hash(columnType, defaultValue);
@override
bool operator ==(Object other) {
return other is ExpressionDartPlaceholderType &&
other.columnType == columnType &&
other.defaultValue == defaultValue;
}
}
class InsertableDartPlaceholderType extends DartPlaceholderType {
final DriftTable? table;
InsertableDartPlaceholderType(this.table);
@override
int get hashCode => table.hashCode;
@override
bool operator ==(Object other) {
return other is InsertableDartPlaceholderType && other.table == table;
}
}
/// A Dart placeholder that will be bound to a dynamically-generated SQL node
/// at runtime.
///
/// Drift supports injecting expressions, order by terms and clauses and limit
/// clauses as placeholders. For insert statements, companions can be used
/// as a Dart placeholder too.
class FoundDartPlaceholder extends FoundElement {
final DartPlaceholderType type;
/// All result sets that are available for this Dart placeholder.
///
/// When queries are operating on multiple tables, especially if some of those
/// tables have aliases, it may be hard to reflect the name of those tables
/// at runtime.
/// For instance, consider this query:
///
/// ```sql
/// myQuery: SELECT a.**, b.** FROM users a
/// INNER JOIN friends f ON f.a_id = a.id
/// INNER JOIN users b ON b.id = f.b_id
/// WHERE $expression;
/// ```
///
/// Here `$expression` is a Dart-defined expression evaluating to an sql
/// boolean.
/// Drift uses to add a `Expression<bool>` parameter to the generated query
/// method. Unfortunately, this puts the burden of picking the right table
/// name on the user. For instance, they may have to use
/// `alias('a', users).someColumn` to avoid getting an runtime exception.
/// With a new build option, drift instead generates a
/// `Expression<bool> Function(Users a, Users b, Friends f)` function as a
/// parameter. This allows users to access the right aliases right away,
/// reducing potential for misuse.
final List<AvailableDriftResultSet> availableResultSets;
@override
final String name;
DartPlaceholder? astNode;
bool get hasDefault =>
type is ExpressionDartPlaceholderType &&
(type as ExpressionDartPlaceholderType).defaultValue != null;
bool get hasDefaultOrImplicitFallback =>
hasDefault ||
(type is SimpleDartPlaceholderType &&
(type as SimpleDartPlaceholderType).kind ==
SimpleDartPlaceholderKind.orderBy);
FoundDartPlaceholder(this.type, this.name, this.availableResultSets);
@override
String get dartParameterName => name;
@override
int get hashCode => Object.hashAll([type, name, ...availableResultSets]);
@override
bool operator ==(Object other) {
return identical(this, other) ||
other is FoundDartPlaceholder &&
other.type == type &&
other.name == name &&
const ListEquality()
.equals(other.availableResultSets, availableResultSets);
}
/// Whether we should write this parameter as a function having available
/// result sets as parameters.
bool writeAsScopedFunction(DriftOptions options) {
return options.scopedDartComponents &&
availableResultSets.isNotEmpty &&
// Don't generate scoped functions for insertables, where the Dart type
// already defines which fields are available
type is! InsertableDartPlaceholderType;
}
}
/// A table or view that is available in the position of a
/// [FoundDartPlaceholder].
///
/// For more information, see [FoundDartPlaceholder.availableResultSets].
class AvailableDriftResultSet {
/// The (potentially aliased) name of this result set.
final String name;
/// The table or view that is available.
final DriftElementWithResultSet entity;
final ResultSetAvailableInStatement? source;
AvailableDriftResultSet(this.name, this.entity, [this.source]);
/// The argument type of this result set when used in a scoped function.
String get argumentType => entity.entityInfoName;
@override
int get hashCode => Object.hash(name, entity);
@override
bool operator ==(Object other) {
return other is AvailableDriftResultSet &&
other.name == name &&
other.entity == entity;
}
}
class _ResultColumnEquality implements Equality<ResultColumn> {
const _ResultColumnEquality();
@override
bool equals(ResultColumn e1, ResultColumn e2) => e1.compatibleTo(e2);
@override
int hash(ResultColumn e) => e.compatibilityHashCode;
@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

@ -1,7 +1,5 @@
import 'package:drift/drift.dart';
import 'element.dart';
import 'table.dart';
import 'query.dart';
class DriftTrigger extends DriftElement {
@override
@ -11,7 +9,7 @@ class DriftTrigger extends DriftElement {
final String createStmt;
/// Writes performed in the body of this trigger.
final List<TriggerTableWrite> writes;
final List<WrittenDriftTable> writes;
DriftTrigger(
super.id,
@ -21,16 +19,3 @@ class DriftTrigger extends DriftElement {
required this.writes,
});
}
/// Information about a write performed by an `INSERT`, `UPDATE` or `DELETE`
/// statement inside a [DriftTrigger] on another [table].
///
/// This information is used to properly invalidate stream queries at runtime,
/// as triggers can cause changes to additional tables after a direct write to
/// another table.
class TriggerTableWrite {
final DriftTable table;
final UpdateKind kind;
TriggerTableWrite(this.table, this.kind);
}

View File

@ -4,6 +4,7 @@ import 'package:drift_dev/src/analysis/results/column.dart';
import 'element.dart';
import 'result_sets.dart';
import 'table.dart';
class DriftView extends DriftElementWithResultSet {
@override
@ -37,6 +38,21 @@ class DriftView extends DriftElementWithResultSet {
required this.nameOfRowClass,
required this.references,
});
/// Obtains all tables transitively referenced by the declaration of this
/// view.
///
/// This includes all tables in [references]. If this view references other
/// views, their [transitiveTableReferences] will be included as well.
Set<DriftTable> get transitiveTableReferences {
return {
for (final reference in references)
if (reference is DriftTable)
reference
else if (reference is DriftView)
...reference.transitiveTableReferences,
};
}
}
abstract class DriftViewSource {}

View File

@ -51,6 +51,7 @@ class ElementSerializer {
'type': 'query',
'sql': element.sql,
'offset': element.sqlOffset,
'result_class': element.resultClassName,
};
} else if (element is DriftTrigger) {
additionalInformation = {
@ -404,6 +405,7 @@ class ElementDeserializer {
references: references,
sql: json['sql'] as String,
sqlOffset: json['offset'] as int,
resultClassName: json['result_class'] as String?,
);
case 'trigger':
return DriftTrigger(
@ -413,7 +415,7 @@ class ElementDeserializer {
createStmt: json['sql'] as String,
writes: [
for (final write in json['writes'])
TriggerTableWrite(
WrittenDriftTable(
await _readElementReference(write['table'] as Map)
as DriftTable,
UpdateKind.values.byName(write['kind'] as String),