mirror of https://github.com/AMT-Cheif/drift.git
Analyze SQL queries
This commit is contained in:
parent
5947956109
commit
b33ef8a220
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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({}, {});
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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),
|
||||
|
|
Loading…
Reference in New Issue