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/dart/helper.dart';
|
||||||
import '../resolver/discover.dart';
|
import '../resolver/discover.dart';
|
||||||
import '../resolver/drift/sqlparser/mapping.dart';
|
import '../resolver/drift/sqlparser/mapping.dart';
|
||||||
|
import '../resolver/file_analysis.dart';
|
||||||
import '../resolver/resolver.dart';
|
import '../resolver/resolver.dart';
|
||||||
import '../results/results.dart';
|
import '../results/results.dart';
|
||||||
import '../serializer.dart';
|
import '../serializer.dart';
|
||||||
|
@ -20,7 +21,7 @@ class DriftAnalysisDriver {
|
||||||
final DriftAnalysisCache cache = DriftAnalysisCache();
|
final DriftAnalysisCache cache = DriftAnalysisCache();
|
||||||
final DriftOptions options;
|
final DriftOptions options;
|
||||||
|
|
||||||
late final TypeMapping typeMapping = TypeMapping(options);
|
late final TypeMapping typeMapping = TypeMapping(this);
|
||||||
late final ElementDeserializer deserializer = ElementDeserializer(this);
|
late final ElementDeserializer deserializer = ElementDeserializer(this);
|
||||||
|
|
||||||
AnalysisResultCacheReader? cacheReader;
|
AnalysisResultCacheReader? cacheReader;
|
||||||
|
@ -157,6 +158,22 @@ class DriftAnalysisDriver {
|
||||||
await _analyzePrepared(known);
|
await _analyzePrepared(known);
|
||||||
return 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 {
|
abstract class AnalysisResultCacheReader {
|
||||||
|
|
|
@ -3,16 +3,18 @@ import 'package:path/path.dart' show url;
|
||||||
import 'package:sqlparser/sqlparser.dart' hide AnalysisError;
|
import 'package:sqlparser/sqlparser.dart' hide AnalysisError;
|
||||||
|
|
||||||
import '../results/element.dart';
|
import '../results/element.dart';
|
||||||
|
import '../results/file_results.dart';
|
||||||
import 'error.dart';
|
import 'error.dart';
|
||||||
|
|
||||||
class FileState {
|
class FileState {
|
||||||
final Uri ownUri;
|
final Uri ownUri;
|
||||||
|
|
||||||
DiscoveredFileState? discovery;
|
DiscoveredFileState? discovery;
|
||||||
final Map<DriftElementId, ElementAnalysisState> analysis = {};
|
|
||||||
|
|
||||||
final List<DriftAnalysisError> errorsDuringDiscovery = [];
|
final List<DriftAnalysisError> errorsDuringDiscovery = [];
|
||||||
|
|
||||||
|
final Map<DriftElementId, ElementAnalysisState> analysis = {};
|
||||||
|
FileAnalysisResult? fileAnalysis;
|
||||||
|
|
||||||
FileState(this.ownUri);
|
FileState(this.ownUri);
|
||||||
|
|
||||||
String get extension => url.extension(ownUri.path);
|
String get extension => url.extension(ownUri.path);
|
||||||
|
|
|
@ -20,7 +20,7 @@ abstract class DriftElementResolver<T extends DiscoveredElement>
|
||||||
context.errors.forEach(reportLint);
|
context.errors.forEach(reportLint);
|
||||||
|
|
||||||
// Also run drift-specific lints on the query
|
// Also run drift-specific lints on the query
|
||||||
final linter = DriftSqlLinter(context, this)..collectLints();
|
final linter = DriftSqlLinter(context)..collectLints();
|
||||||
linter.sqlParserErrors.forEach(reportLint);
|
linter.sqlParserErrors.forEach(reportLint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,18 +51,7 @@ abstract class DriftElementResolver<T extends DiscoveredElement>
|
||||||
}
|
}
|
||||||
|
|
||||||
SqlEngine newEngineWithTables(Iterable<DriftElement> references) {
|
SqlEngine newEngineWithTables(Iterable<DriftElement> references) {
|
||||||
final driver = resolver.driver;
|
return resolver.driver.typeMapping.newEngineWithTables(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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<DriftElement>> resolveSqlReferences(AstNode stmt) async {
|
Future<List<DriftElement>> resolveSqlReferences(AstNode stmt) async {
|
||||||
|
|
|
@ -12,12 +12,10 @@ class DriftQueryResolver
|
||||||
final stmt = discovered.sqlNode.statement;
|
final stmt = discovered.sqlNode.statement;
|
||||||
final references = await resolveSqlReferences(stmt);
|
final references = await resolveSqlReferences(stmt);
|
||||||
|
|
||||||
final engine = newEngineWithTables(references);
|
|
||||||
|
|
||||||
final source = (file.discovery as DiscoveredDriftFile).originalSource;
|
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(
|
return DefinedSqlQuery(
|
||||||
discovered.ownId,
|
discovered.ownId,
|
||||||
DriftDeclaration.driftFile(stmt, file.ownUri),
|
DriftDeclaration.driftFile(stmt, file.ownUri),
|
||||||
|
|
|
@ -1,16 +1,13 @@
|
||||||
import 'package:sqlparser/sqlparser.dart';
|
import 'package:sqlparser/sqlparser.dart';
|
||||||
|
|
||||||
import '../../resolver.dart';
|
|
||||||
|
|
||||||
/// Implements (mostly drift-specific) lints for SQL statements that aren't
|
/// Implements (mostly drift-specific) lints for SQL statements that aren't
|
||||||
/// implementeed in `sqlparser`.
|
/// implementeed in `sqlparser`.
|
||||||
class DriftSqlLinter {
|
class DriftSqlLinter {
|
||||||
final AnalysisContext _context;
|
final AnalysisContext _context;
|
||||||
final LocalElementResolver _resolver;
|
|
||||||
|
|
||||||
final List<AnalysisError> sqlParserErrors = [];
|
final List<AnalysisError> sqlParserErrors = [];
|
||||||
|
|
||||||
DriftSqlLinter(this._context, this._resolver);
|
DriftSqlLinter(this._context);
|
||||||
|
|
||||||
void collectLints() {
|
void collectLints() {
|
||||||
_context.root.acceptWithoutArg(_LintingVisitor(this));
|
_context.root.acceptWithoutArg(_LintingVisitor(this));
|
||||||
|
|
|
@ -1,15 +1,29 @@
|
||||||
import 'package:drift/drift.dart' show DriftSqlType;
|
import 'package:drift/drift.dart' show DriftSqlType;
|
||||||
import 'package:sqlparser/sqlparser.dart';
|
import 'package:sqlparser/sqlparser.dart';
|
||||||
|
|
||||||
import '../../../../analyzer/options.dart';
|
import '../../../driver/driver.dart';
|
||||||
import '../../../results/results.dart';
|
import '../../../results/results.dart';
|
||||||
|
|
||||||
/// Converts tables and types between `drift_dev` internal reprensentation and
|
/// Converts tables and types between `drift_dev` internal reprensentation and
|
||||||
/// the one used by the `sqlparser` package.
|
/// the one used by the `sqlparser` package.
|
||||||
class TypeMapping {
|
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) {
|
Table asSqlParserTable(DriftTable table) {
|
||||||
return Table(
|
return Table(
|
||||||
|
@ -61,7 +75,7 @@ class TypeMapping {
|
||||||
type: BasicType.int, hint: overrideHint ?? const IsBoolean());
|
type: BasicType.int, hint: overrideHint ?? const IsBoolean());
|
||||||
case DriftSqlType.dateTime:
|
case DriftSqlType.dateTime:
|
||||||
return ResolvedType(
|
return ResolvedType(
|
||||||
type: options.storeDateTimeValuesAsText
|
type: driver.options.storeDateTimeValuesAsText
|
||||||
? BasicType.text
|
? BasicType.text
|
||||||
: BasicType.int,
|
: BasicType.int,
|
||||||
hint: overrideHint ?? const IsDateTime(),
|
hint: overrideHint ?? const IsDateTime(),
|
||||||
|
@ -85,7 +99,7 @@ class TypeMapping {
|
||||||
case BasicType.int:
|
case BasicType.int:
|
||||||
if (type.hint is IsBoolean) {
|
if (type.hint is IsBoolean) {
|
||||||
return DriftSqlType.bool;
|
return DriftSqlType.bool;
|
||||||
} else if (!options.storeDateTimeValuesAsText &&
|
} else if (!driver.options.storeDateTimeValuesAsText &&
|
||||||
type.hint is IsDateTime) {
|
type.hint is IsDateTime) {
|
||||||
return DriftSqlType.dateTime;
|
return DriftSqlType.dateTime;
|
||||||
} else if (type.hint is IsBigInt) {
|
} else if (type.hint is IsBigInt) {
|
||||||
|
@ -95,7 +109,8 @@ class TypeMapping {
|
||||||
case BasicType.real:
|
case BasicType.real:
|
||||||
return DriftSqlType.double;
|
return DriftSqlType.double;
|
||||||
case BasicType.text:
|
case BasicType.text:
|
||||||
if (options.storeDateTimeValuesAsText && type.hint is IsDateTime) {
|
if (driver.options.storeDateTimeValuesAsText &&
|
||||||
|
type.hint is IsDateTime) {
|
||||||
return DriftSqlType.dateTime;
|
return DriftSqlType.dateTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ class DriftTriggerResolver
|
||||||
final context = engine.analyzeNode(stmt, source);
|
final context = engine.analyzeNode(stmt, source);
|
||||||
reportLints(context);
|
reportLints(context);
|
||||||
|
|
||||||
TriggerTableWrite? mapWrite(TableWrite parserWrite) {
|
WrittenDriftTable? mapWrite(TableWrite parserWrite) {
|
||||||
drift.UpdateKind kind;
|
drift.UpdateKind kind;
|
||||||
switch (parserWrite.kind) {
|
switch (parserWrite.kind) {
|
||||||
case UpdateKind.insert:
|
case UpdateKind.insert:
|
||||||
|
@ -40,7 +40,7 @@ class DriftTriggerResolver
|
||||||
.whereType<DriftTable>()
|
.whereType<DriftTable>()
|
||||||
.firstWhereOrNull((e) => e.schemaName == parserWrite.table.name);
|
.firstWhereOrNull((e) => e.schemaName == parserWrite.table.name);
|
||||||
if (table != null) {
|
if (table != null) {
|
||||||
return TriggerTableWrite(table, kind);
|
return WrittenDriftTable(table, kind);
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -53,7 +53,7 @@ class DriftTriggerResolver
|
||||||
createStmt: source.substring(stmt.firstPosition, stmt.lastPosition),
|
createStmt: source.substring(stmt.firstPosition, stmt.lastPosition),
|
||||||
writes: findWrittenTables(stmt)
|
writes: findWrittenTables(stmt)
|
||||||
.map(mapWrite)
|
.map(mapWrite)
|
||||||
.whereType<TriggerTableWrite>()
|
.whereType<WrittenDriftTable>()
|
||||||
.toList(),
|
.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
|
/// analysis happens during code generation because intermediate state is hard
|
||||||
/// to serialize and there are little benefits of analyzing queries early.
|
/// to serialize and there are little benefits of analyzing queries early.
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class QueryOnAccessor {
|
class QueryOnAccessor implements DriftQueryDeclaration {
|
||||||
|
@override
|
||||||
final String name;
|
final String name;
|
||||||
final String sql;
|
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 '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
|
/// A named SQL query defined in a `.drift` file. A later compile step will
|
||||||
/// further analyze this query and run analysis on it.
|
/// 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
|
/// since they are local elements which can't be referenced by others, there's
|
||||||
/// no clear advantage wrt. incremental compilation if queries are fully
|
/// no clear advantage wrt. incremental compilation if queries are fully
|
||||||
/// analyzed and serialized. So, we just do this in the generator.
|
/// 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.
|
/// The unmodified source of the declared SQL statement forming this query.
|
||||||
final String sql;
|
final String sql;
|
||||||
|
|
||||||
|
final String? resultClassName;
|
||||||
|
|
||||||
/// The offset of [sql] in the source file, used to properly report errors
|
/// The offset of [sql] in the source file, used to properly report errors
|
||||||
/// later.
|
/// later.
|
||||||
final int sqlOffset;
|
final int sqlOffset;
|
||||||
|
@ -22,11 +40,823 @@ class DefinedSqlQuery extends DriftElement {
|
||||||
@override
|
@override
|
||||||
final List<DriftElement> references;
|
final List<DriftElement> references;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get name => id.name;
|
||||||
|
|
||||||
DefinedSqlQuery(
|
DefinedSqlQuery(
|
||||||
super.id,
|
super.id,
|
||||||
super.declaration, {
|
super.declaration, {
|
||||||
required this.references,
|
required this.references,
|
||||||
required this.sql,
|
required this.sql,
|
||||||
required this.sqlOffset,
|
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 'element.dart';
|
||||||
import 'table.dart';
|
import 'query.dart';
|
||||||
|
|
||||||
class DriftTrigger extends DriftElement {
|
class DriftTrigger extends DriftElement {
|
||||||
@override
|
@override
|
||||||
|
@ -11,7 +9,7 @@ class DriftTrigger extends DriftElement {
|
||||||
final String createStmt;
|
final String createStmt;
|
||||||
|
|
||||||
/// Writes performed in the body of this trigger.
|
/// Writes performed in the body of this trigger.
|
||||||
final List<TriggerTableWrite> writes;
|
final List<WrittenDriftTable> writes;
|
||||||
|
|
||||||
DriftTrigger(
|
DriftTrigger(
|
||||||
super.id,
|
super.id,
|
||||||
|
@ -21,16 +19,3 @@ class DriftTrigger extends DriftElement {
|
||||||
required this.writes,
|
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 'element.dart';
|
||||||
import 'result_sets.dart';
|
import 'result_sets.dart';
|
||||||
|
import 'table.dart';
|
||||||
|
|
||||||
class DriftView extends DriftElementWithResultSet {
|
class DriftView extends DriftElementWithResultSet {
|
||||||
@override
|
@override
|
||||||
|
@ -37,6 +38,21 @@ class DriftView extends DriftElementWithResultSet {
|
||||||
required this.nameOfRowClass,
|
required this.nameOfRowClass,
|
||||||
required this.references,
|
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 {}
|
abstract class DriftViewSource {}
|
||||||
|
|
|
@ -51,6 +51,7 @@ class ElementSerializer {
|
||||||
'type': 'query',
|
'type': 'query',
|
||||||
'sql': element.sql,
|
'sql': element.sql,
|
||||||
'offset': element.sqlOffset,
|
'offset': element.sqlOffset,
|
||||||
|
'result_class': element.resultClassName,
|
||||||
};
|
};
|
||||||
} else if (element is DriftTrigger) {
|
} else if (element is DriftTrigger) {
|
||||||
additionalInformation = {
|
additionalInformation = {
|
||||||
|
@ -404,6 +405,7 @@ class ElementDeserializer {
|
||||||
references: references,
|
references: references,
|
||||||
sql: json['sql'] as String,
|
sql: json['sql'] as String,
|
||||||
sqlOffset: json['offset'] as int,
|
sqlOffset: json['offset'] as int,
|
||||||
|
resultClassName: json['result_class'] as String?,
|
||||||
);
|
);
|
||||||
case 'trigger':
|
case 'trigger':
|
||||||
return DriftTrigger(
|
return DriftTrigger(
|
||||||
|
@ -413,7 +415,7 @@ class ElementDeserializer {
|
||||||
createStmt: json['sql'] as String,
|
createStmt: json['sql'] as String,
|
||||||
writes: [
|
writes: [
|
||||||
for (final write in json['writes'])
|
for (final write in json['writes'])
|
||||||
TriggerTableWrite(
|
WrittenDriftTable(
|
||||||
await _readElementReference(write['table'] as Map)
|
await _readElementReference(write['table'] as Map)
|
||||||
as DriftTable,
|
as DriftTable,
|
||||||
UpdateKind.values.byName(write['kind'] as String),
|
UpdateKind.values.byName(write['kind'] as String),
|
||||||
|
|
Loading…
Reference in New Issue