mirror of https://github.com/AMT-Cheif/drift.git
Merge pull request #1638 from LeFrosch/develop
Support for nested queries like (#1634)
This commit is contained in:
commit
36815c0b88
|
@ -195,7 +195,7 @@ const additionalPostgresKeywords = <String>{
|
||||||
/// [sqliteKeywords].
|
/// [sqliteKeywords].
|
||||||
bool isSqliteKeyword(String s) => sqliteKeywords.contains(s.toUpperCase());
|
bool isSqliteKeyword(String s) => sqliteKeywords.contains(s.toUpperCase());
|
||||||
|
|
||||||
final _whitespace = RegExp(r'\s');
|
final _notInKeyword = RegExp('[^A-Za-z_0-9]');
|
||||||
|
|
||||||
/// Escapes [s] by wrapping it in backticks if it's an sqlite keyword.
|
/// Escapes [s] by wrapping it in backticks if it's an sqlite keyword.
|
||||||
String escapeIfNeeded(String s, [SqlDialect dialect = SqlDialect.sqlite]) {
|
String escapeIfNeeded(String s, [SqlDialect dialect = SqlDialect.sqlite]) {
|
||||||
|
@ -206,6 +206,6 @@ String escapeIfNeeded(String s, [SqlDialect dialect = SqlDialect.sqlite]) {
|
||||||
isKeyword |= additionalPostgresKeywords.contains(inUpperCase);
|
isKeyword |= additionalPostgresKeywords.contains(inUpperCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isKeyword || s.contains(_whitespace)) return '"$s"';
|
if (isKeyword || _notInKeyword.hasMatch(s)) return '"$s"';
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
// Mega compilation unit that includes all Dart apis related to generating SQL
|
// Mega compilation unit that includes all Dart apis related to generating SQL
|
||||||
// at runtime.
|
// at runtime.
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:drift/sqlite_keywords.dart';
|
import 'package:drift/sqlite_keywords.dart';
|
||||||
|
|
|
@ -253,6 +253,13 @@ abstract class Selectable<T>
|
||||||
Selectable<N> map<N>(N Function(T) mapper) {
|
Selectable<N> map<N>(N Function(T) mapper) {
|
||||||
return _MappedSelectable<T, N>(this, mapper);
|
return _MappedSelectable<T, N>(this, mapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Maps this selectable by the [mapper] function.
|
||||||
|
///
|
||||||
|
/// Like [map] just async.
|
||||||
|
Selectable<N> asyncMap<N>(Future<N> Function(T) mapper) {
|
||||||
|
return _AsyncMappedSelectable<T, N>(this, mapper);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MappedSelectable<S, T> extends Selectable<T> {
|
class _MappedSelectable<S, T> extends Selectable<T> {
|
||||||
|
@ -274,6 +281,26 @@ class _MappedSelectable<S, T> extends Selectable<T> {
|
||||||
List<T> _mapResults(List<S> results) => results.map(_mapper).toList();
|
List<T> _mapResults(List<S> results) => results.map(_mapper).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _AsyncMappedSelectable<S, T> extends Selectable<T> {
|
||||||
|
final Selectable<S> _source;
|
||||||
|
final Future<T> Function(S) _mapper;
|
||||||
|
|
||||||
|
_AsyncMappedSelectable(this._source, this._mapper);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<T>> get() {
|
||||||
|
return _source.get().then(_mapResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<List<T>> watch() {
|
||||||
|
return _source.watch().asyncMap(_mapResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<T>> _mapResults(List<S> results) async =>
|
||||||
|
[for (final result in results) await _mapper(result)];
|
||||||
|
}
|
||||||
|
|
||||||
/// Mixin for a [Query] that operates on a single primary table only.
|
/// Mixin for a [Query] that operates on a single primary table only.
|
||||||
mixin SingleTableQueryMixin<T extends HasResultSet, D> on Query<T, D> {
|
mixin SingleTableQueryMixin<T extends HasResultSet, D> on Query<T, D> {
|
||||||
/// Makes this statement only include rows that match the [filter].
|
/// Makes this statement only include rows that match the [filter].
|
||||||
|
|
|
@ -1814,6 +1814,32 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
|
||||||
updates: {config}).then((rows) => rows.map(config.mapFromRow).toList());
|
updates: {config}).then((rows) => rows.map(config.mapFromRow).toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Selectable<NestedResult> nested(String? var1) {
|
||||||
|
return customSelect(
|
||||||
|
'SELECT"defaults"."a" AS "nested_0.a", "defaults"."b" AS "nested_0.b", defaults.b AS "\$n_0" FROM with_defaults AS defaults WHERE a = ?1',
|
||||||
|
variables: [
|
||||||
|
Variable<String?>(var1)
|
||||||
|
],
|
||||||
|
readsFrom: {
|
||||||
|
withConstraints,
|
||||||
|
withDefaults,
|
||||||
|
}).asyncMap((QueryRow row) async {
|
||||||
|
return NestedResult(
|
||||||
|
row: row,
|
||||||
|
defaults: withDefaults.mapFromRow(row, tablePrefix: 'nested_0'),
|
||||||
|
nestedQuery0: await customSelect(
|
||||||
|
'SELECT * FROM with_constraints AS c WHERE c.b = ?1',
|
||||||
|
variables: [
|
||||||
|
Variable<String>(row.read('\$n_0'))
|
||||||
|
],
|
||||||
|
readsFrom: {
|
||||||
|
withConstraints,
|
||||||
|
withDefaults,
|
||||||
|
}).map(withConstraints.mapFromRow).get(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Future<int> writeConfig({required String key, String? value}) {
|
Future<int> writeConfig({required String key, String? value}) {
|
||||||
return customInsert(
|
return customInsert(
|
||||||
'REPLACE INTO config (config_key, config_value) VALUES (?1, ?2)',
|
'REPLACE INTO config (config_key, config_value) VALUES (?1, ?2)',
|
||||||
|
@ -1953,3 +1979,29 @@ class ReadRowIdResult extends CustomResultSet {
|
||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class NestedResult extends CustomResultSet {
|
||||||
|
final WithDefault defaults;
|
||||||
|
final List<WithConstraint> nestedQuery0;
|
||||||
|
NestedResult({
|
||||||
|
required QueryRow row,
|
||||||
|
required this.defaults,
|
||||||
|
required this.nestedQuery0,
|
||||||
|
}) : super(row);
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(defaults, nestedQuery0);
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
(other is NestedResult &&
|
||||||
|
other.defaults == this.defaults &&
|
||||||
|
other.nestedQuery0 == this.nestedQuery0);
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return (StringBuffer('NestedResult(')
|
||||||
|
..write('defaults: $defaults, ')
|
||||||
|
..write('nestedQuery0: $nestedQuery0')
|
||||||
|
..write(')'))
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -93,3 +93,7 @@ cfeTest: WITH RECURSIVE
|
||||||
|
|
||||||
nullableQuery: SELECT MAX(oid) FROM config;
|
nullableQuery: SELECT MAX(oid) FROM config;
|
||||||
addConfig: INSERT INTO config $value RETURNING *;
|
addConfig: INSERT INTO config $value RETURNING *;
|
||||||
|
|
||||||
|
nested: SELECT defaults.**, LIST(SELECT * FROM with_constraints c WHERE c.b = defaults.b)
|
||||||
|
FROM with_defaults defaults
|
||||||
|
WHERE a = ?;
|
||||||
|
|
|
@ -4,7 +4,6 @@ import 'package:analyzer/dart/element/element.dart';
|
||||||
import 'package:analyzer/dart/element/nullability_suffix.dart';
|
import 'package:analyzer/dart/element/nullability_suffix.dart';
|
||||||
import 'package:analyzer/dart/element/type.dart';
|
import 'package:analyzer/dart/element/type.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:drift/sqlite_keywords.dart';
|
|
||||||
import 'package:drift_dev/moor_generator.dart';
|
import 'package:drift_dev/moor_generator.dart';
|
||||||
import 'package:drift_dev/src/analyzer/errors.dart';
|
import 'package:drift_dev/src/analyzer/errors.dart';
|
||||||
import 'package:drift_dev/src/analyzer/runner/steps.dart';
|
import 'package:drift_dev/src/analyzer/runner/steps.dart';
|
||||||
|
|
|
@ -18,7 +18,7 @@ class TableParser {
|
||||||
final table = MoorTable(
|
final table = MoorTable(
|
||||||
fromClass: element,
|
fromClass: element,
|
||||||
columns: columns,
|
columns: columns,
|
||||||
sqlName: escapeIfNeeded(sqlName),
|
sqlName: sqlName,
|
||||||
dartTypeName: dataClassInfo.enforcedName,
|
dartTypeName: dataClassInfo.enforcedName,
|
||||||
existingRowClass: dataClassInfo.existingClass,
|
existingRowClass: dataClassInfo.existingClass,
|
||||||
primaryKey: primaryKey,
|
primaryKey: primaryKey,
|
||||||
|
|
|
@ -83,6 +83,8 @@ class _LintingVisitor extends RecursiveVisitor<void, void> {
|
||||||
return visitDartPlaceholder(e, arg);
|
return visitDartPlaceholder(e, arg);
|
||||||
} else if (e is NestedStarResultColumn) {
|
} else if (e is NestedStarResultColumn) {
|
||||||
return visitResultColumn(e, arg);
|
return visitResultColumn(e, arg);
|
||||||
|
} else if (e is NestedQueryColumn) {
|
||||||
|
return visitResultColumn(e, arg);
|
||||||
}
|
}
|
||||||
|
|
||||||
visitChildren(e, arg);
|
visitChildren(e, arg);
|
||||||
|
@ -160,6 +162,20 @@ class _LintingVisitor extends RecursiveVisitor<void, void> {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (e is NestedQueryColumn) {
|
||||||
|
// check that a LIST(...) column only appears in a top-level select
|
||||||
|
// statement
|
||||||
|
if (!linter.contextRootIsQuery || e.parent != linter.context.root) {
|
||||||
|
linter.lints.add(AnalysisError(
|
||||||
|
type: AnalysisErrorType.other,
|
||||||
|
message: 'Nested query may only appear in a top-level select '
|
||||||
|
"query. They're not supported in compound selects or select "
|
||||||
|
'expressions',
|
||||||
|
relevantNode: e,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
@ -0,0 +1,179 @@
|
||||||
|
import 'package:drift_dev/src/model/model.dart';
|
||||||
|
import 'package:sqlparser/sqlparser.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 visitMoorSpecificNode(MoorSpecificNode 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.visitMoorSpecificNode(e, childState);
|
||||||
|
childState._process();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
super.visitMoorSpecificNode(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? visitMoorSpecificNode(
|
||||||
|
MoorSpecificNode 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.visitMoorSpecificNode(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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -167,8 +167,8 @@ class SqlAnalyzer extends BaseAnalyzer {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final handled =
|
final handled =
|
||||||
QueryHandler(query, context, mapper, requiredVariables: variables)
|
QueryHandler(context, mapper, requiredVariables: variables)
|
||||||
.handle()
|
.handle(query)
|
||||||
..declaredInMoorFile = query is DeclaredMoorQuery;
|
..declaredInMoorFile = query is DeclaredMoorQuery;
|
||||||
foundQueries.add(handled);
|
foundQueries.add(handled);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
|
|
|
@ -1,59 +1,104 @@
|
||||||
import 'package:drift_dev/moor_generator.dart';
|
import 'package:drift_dev/moor_generator.dart';
|
||||||
import 'package:drift_dev/src/analyzer/sql_queries/type_mapping.dart';
|
import 'package:drift_dev/src/analyzer/sql_queries/type_mapping.dart';
|
||||||
import 'package:drift_dev/src/utils/type_converter_hint.dart';
|
import 'package:drift_dev/src/utils/type_converter_hint.dart';
|
||||||
|
import 'package:recase/recase.dart';
|
||||||
import 'package:sqlparser/sqlparser.dart' hide ResultColumn;
|
import 'package:sqlparser/sqlparser.dart' hide ResultColumn;
|
||||||
import 'package:sqlparser/utils/find_referenced_tables.dart';
|
import 'package:sqlparser/utils/find_referenced_tables.dart';
|
||||||
|
|
||||||
import 'lints/linter.dart';
|
import 'lints/linter.dart';
|
||||||
|
import 'nested_queries.dart';
|
||||||
import 'required_variables.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
|
/// Maps an [AnalysisContext] from the sqlparser to a [SqlQuery] from this
|
||||||
/// generator package by determining its type, return columns, variables and so
|
/// generator package by determining its type, return columns, variables and so
|
||||||
/// on.
|
/// on.
|
||||||
class QueryHandler {
|
class QueryHandler {
|
||||||
final DeclaredQuery source;
|
|
||||||
final AnalysisContext context;
|
final AnalysisContext context;
|
||||||
final TypeMapper mapper;
|
final TypeMapper mapper;
|
||||||
final RequiredVariables requiredVariables;
|
final RequiredVariables requiredVariables;
|
||||||
|
|
||||||
|
/// 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<Table> _foundTables;
|
||||||
late Set<View> _foundViews;
|
late Set<View> _foundViews;
|
||||||
late List<FoundElement> _foundElements;
|
|
||||||
|
|
||||||
Iterable<FoundVariable> get _foundVariables =>
|
/// Used to create a unique name for every nested query. This needs to be
|
||||||
_foundElements.whereType<FoundVariable>();
|
/// shared between queries, therefore this should not be part of the
|
||||||
|
/// context.
|
||||||
|
int nestedQueryCounter;
|
||||||
|
|
||||||
BaseSelectStatement get _select => context.root as BaseSelectStatement;
|
QueryHandler(
|
||||||
|
this.context,
|
||||||
|
this.mapper, {
|
||||||
|
this.requiredVariables = RequiredVariables.empty,
|
||||||
|
}) : nestedQueryCounter = 0;
|
||||||
|
|
||||||
QueryHandler(this.source, this.context, this.mapper,
|
SqlQuery handle(DeclaredQuery source) {
|
||||||
{this.requiredVariables = RequiredVariables.empty});
|
final nestedAnalyzer = NestedQueryAnalyzer();
|
||||||
|
NestedQueriesContainer? nestedScope;
|
||||||
|
|
||||||
String get name => source.name;
|
if (context.root is SelectStatement) {
|
||||||
|
nestedScope = nestedAnalyzer.analyzeRoot(context.root as SelectStatement);
|
||||||
|
}
|
||||||
|
|
||||||
SqlQuery handle() {
|
final foundElements = mapper.extractElements(
|
||||||
_foundElements =
|
ctx: context,
|
||||||
mapper.extractElements(context, required: requiredVariables);
|
root: context.root,
|
||||||
|
required: requiredVariables,
|
||||||
|
nestedScope: nestedScope,
|
||||||
|
);
|
||||||
|
_verifyNoSkippedIndexes(foundElements);
|
||||||
|
|
||||||
_verifyNoSkippedIndexes();
|
final String? requestedResultClass;
|
||||||
final query = _mapToMoor();
|
if (source is DeclaredMoorQuery) {
|
||||||
|
requestedResultClass = source.astNode.as;
|
||||||
|
} else {
|
||||||
|
requestedResultClass = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final query = _mapToMoor(_QueryHandlerContext(
|
||||||
|
foundElements: foundElements,
|
||||||
|
queryName: source.name,
|
||||||
|
requestedResultClass: requestedResultClass,
|
||||||
|
root: context.root,
|
||||||
|
nestedScope: nestedScope,
|
||||||
|
));
|
||||||
|
|
||||||
final linter = Linter.forHandler(this);
|
final linter = Linter.forHandler(this);
|
||||||
linter.reportLints();
|
linter.reportLints();
|
||||||
query.lints = linter.lints;
|
query.lints = [...nestedAnalyzer.errors, ...linter.lints];
|
||||||
|
|
||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
SqlQuery _mapToMoor() {
|
SqlQuery _mapToMoor(_QueryHandlerContext queryContext) {
|
||||||
final root = context.root;
|
if (queryContext.root is BaseSelectStatement) {
|
||||||
if (root is BaseSelectStatement) {
|
return _handleSelect(queryContext);
|
||||||
return _handleSelect();
|
} else if (queryContext.root is UpdateStatement ||
|
||||||
} else if (root is UpdateStatement ||
|
queryContext.root is DeleteStatement ||
|
||||||
root is DeleteStatement ||
|
queryContext.root is InsertStatement) {
|
||||||
root is InsertStatement) {
|
return _handleUpdate(queryContext);
|
||||||
return _handleUpdate();
|
|
||||||
} else {
|
} else {
|
||||||
throw StateError('Unexpected sql: Got $root, expected insert, select, '
|
throw StateError(
|
||||||
|
'Unexpected sql: Got ${queryContext.root}, expected insert, select, '
|
||||||
'update or delete');
|
'update or delete');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -63,26 +108,28 @@ class QueryHandler {
|
||||||
_foundViews = visitor.foundViews;
|
_foundViews = visitor.foundViews;
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdatingQuery _handleUpdate() {
|
UpdatingQuery _handleUpdate(_QueryHandlerContext queryContext) {
|
||||||
|
final root = queryContext.root;
|
||||||
|
|
||||||
final updatedFinder = UpdatedTablesVisitor();
|
final updatedFinder = UpdatedTablesVisitor();
|
||||||
context.root.acceptWithoutArg(updatedFinder);
|
root.acceptWithoutArg(updatedFinder);
|
||||||
_applyFoundTables(updatedFinder);
|
_applyFoundTables(updatedFinder);
|
||||||
|
|
||||||
final root = context.root;
|
|
||||||
final isInsert = root is InsertStatement;
|
final isInsert = root is InsertStatement;
|
||||||
|
|
||||||
InferredResultSet? resultSet;
|
InferredResultSet? resultSet;
|
||||||
if (root is StatementReturningColumns) {
|
if (root is StatementReturningColumns) {
|
||||||
final columns = root.returnedResultSet?.resolvedColumns;
|
final columns = root.returnedResultSet?.resolvedColumns;
|
||||||
if (columns != null) {
|
if (columns != null) {
|
||||||
resultSet = _inferResultSet(columns);
|
resultSet = _inferResultSet(queryContext, columns);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return UpdatingQuery(
|
return UpdatingQuery(
|
||||||
name,
|
queryContext.queryName,
|
||||||
context,
|
context,
|
||||||
_foundElements,
|
root,
|
||||||
|
queryContext.foundElements,
|
||||||
updatedFinder.writtenTables
|
updatedFinder.writtenTables
|
||||||
.map(mapper.writtenToMoor)
|
.map(mapper.writtenToMoor)
|
||||||
.whereType<WrittenMoorTable>()
|
.whereType<WrittenMoorTable>()
|
||||||
|
@ -93,9 +140,10 @@ class QueryHandler {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
SqlSelectQuery _handleSelect() {
|
SqlSelectQuery _handleSelect(_QueryHandlerContext queryContext) {
|
||||||
final tableFinder = ReferencedTablesVisitor();
|
final tableFinder = ReferencedTablesVisitor();
|
||||||
_select.acceptWithoutArg(tableFinder);
|
queryContext.root.acceptWithoutArg(tableFinder);
|
||||||
|
|
||||||
_applyFoundTables(tableFinder);
|
_applyFoundTables(tableFinder);
|
||||||
|
|
||||||
final moorTables =
|
final moorTables =
|
||||||
|
@ -105,22 +153,25 @@ class QueryHandler {
|
||||||
|
|
||||||
final moorEntities = [...moorTables, ...moorViews];
|
final moorEntities = [...moorTables, ...moorViews];
|
||||||
|
|
||||||
String? requestedName;
|
|
||||||
if (source is DeclaredMoorQuery) {
|
|
||||||
requestedName = (source as DeclaredMoorQuery).astNode.as;
|
|
||||||
}
|
|
||||||
|
|
||||||
return SqlSelectQuery(
|
return SqlSelectQuery(
|
||||||
name,
|
queryContext.queryName,
|
||||||
context,
|
context,
|
||||||
_foundElements,
|
queryContext.root,
|
||||||
|
queryContext.foundElements,
|
||||||
moorEntities,
|
moorEntities,
|
||||||
_inferResultSet(_select.resolvedColumns!),
|
_inferResultSet(
|
||||||
requestedName,
|
queryContext,
|
||||||
|
(queryContext.root as BaseSelectStatement).resolvedColumns!,
|
||||||
|
),
|
||||||
|
queryContext.requestedResultClass,
|
||||||
|
queryContext.nestedScope,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
InferredResultSet _inferResultSet(List<Column> rawColumns) {
|
InferredResultSet _inferResultSet(
|
||||||
|
_QueryHandlerContext queryContext,
|
||||||
|
List<Column> rawColumns,
|
||||||
|
) {
|
||||||
final candidatesForSingleTable = {..._foundTables, ..._foundViews};
|
final candidatesForSingleTable = {..._foundTables, ..._foundViews};
|
||||||
final columns = <ResultColumn>[];
|
final columns = <ResultColumn>[];
|
||||||
|
|
||||||
|
@ -140,7 +191,7 @@ class QueryHandler {
|
||||||
candidatesForSingleTable.removeWhere((t) => t != resultSet);
|
candidatesForSingleTable.removeWhere((t) => t != resultSet);
|
||||||
}
|
}
|
||||||
|
|
||||||
final nestedResults = _findNestedResultTables();
|
final nestedResults = _findNestedResultTables(queryContext);
|
||||||
if (nestedResults.isNotEmpty) {
|
if (nestedResults.isNotEmpty) {
|
||||||
// The single table optimization doesn't make sense when nested result
|
// The single table optimization doesn't make sense when nested result
|
||||||
// sets are present.
|
// sets are present.
|
||||||
|
@ -199,15 +250,21 @@ class QueryHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return InferredResultSet(null, columns, nestedResults: nestedResults);
|
return InferredResultSet(
|
||||||
|
null,
|
||||||
|
columns,
|
||||||
|
nestedResults: nestedResults,
|
||||||
|
resultClassName: queryContext.requestedResultClass,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<NestedResultTable> _findNestedResultTables() {
|
List<NestedResult> _findNestedResultTables(
|
||||||
final query = context.root;
|
_QueryHandlerContext queryContext) {
|
||||||
// We don't currently support nested results for compound statements
|
// We don't currently support nested results for compound statements
|
||||||
if (query is! SelectStatement) return const [];
|
if (queryContext.root is! SelectStatement) return const [];
|
||||||
|
final query = queryContext.root as SelectStatement;
|
||||||
|
|
||||||
final nestedTables = <NestedResultTable>[];
|
final nestedTables = <NestedResult>[];
|
||||||
final analysis = JoinModel.of(query);
|
final analysis = JoinModel.of(query);
|
||||||
|
|
||||||
for (final column in query.columns) {
|
for (final column in query.columns) {
|
||||||
|
@ -219,8 +276,45 @@ class QueryHandler {
|
||||||
final moorTable = mapper.viewOrTableToMoor(result)!;
|
final moorTable = mapper.viewOrTableToMoor(result)!;
|
||||||
final isNullable =
|
final isNullable =
|
||||||
analysis == null || analysis.isNullableTable(originalResult!);
|
analysis == null || analysis.isNullableTable(originalResult!);
|
||||||
nestedTables.add(NestedResultTable(column, column.tableName, moorTable,
|
nestedTables.add(NestedResultTable(
|
||||||
isNullable: isNullable));
|
column,
|
||||||
|
column.as ?? column.tableName,
|
||||||
|
moorTable,
|
||||||
|
isNullable: isNullable,
|
||||||
|
));
|
||||||
|
} else if (column is NestedQueryColumn) {
|
||||||
|
final childScope = queryContext.nestedScope?.nestedQueries[column];
|
||||||
|
|
||||||
|
final foundElements = mapper.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,
|
||||||
|
)),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -265,8 +359,8 @@ class QueryHandler {
|
||||||
/// We verify that no variable numbers are skipped in the query. For instance,
|
/// 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
|
/// `SELECT * FROM tbl WHERE a = ?2 AND b = ?` would fail this check because
|
||||||
/// the index 1 is never used.
|
/// the index 1 is never used.
|
||||||
void _verifyNoSkippedIndexes() {
|
void _verifyNoSkippedIndexes(List<FoundElement> foundElements) {
|
||||||
final variables = List.of(_foundVariables)
|
final variables = List.of(foundElements.whereType<FoundVariable>())
|
||||||
..sort((a, b) => a.index.compareTo(b.index));
|
..sort((a, b) => a.index.compareTo(b.index));
|
||||||
|
|
||||||
var currentExpectedIndex = 1;
|
var currentExpectedIndex = 1;
|
||||||
|
|
|
@ -107,11 +107,11 @@ class TypeMapper {
|
||||||
return engineView;
|
return engineView;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extracts variables and Dart templates from the [ctx]. Variables are
|
/// Extracts variables and Dart templates from the AST tree starting at
|
||||||
/// sorted by their ascending index. Placeholders are sorted by the position
|
/// [root], but nested queries are excluded. Variables are sorted by their
|
||||||
/// they have in the query. When comparing variables and placeholders, the
|
/// ascending index. Placeholders are sorted by the position they have in the
|
||||||
/// variable comes first if the first variable with the same index appears
|
/// query. When comparing variables and placeholders, the variable comes first
|
||||||
/// before the placeholder.
|
/// if the first variable with the same index appears before the placeholder.
|
||||||
///
|
///
|
||||||
/// Additionally, the following assumptions can be made if this method returns
|
/// Additionally, the following assumptions can be made if this method returns
|
||||||
/// without throwing:
|
/// without throwing:
|
||||||
|
@ -120,14 +120,19 @@ class TypeMapper {
|
||||||
/// a Dart placeholder, its indexed is LOWER than that element. This means
|
/// a Dart placeholder, its indexed is LOWER than that element. This means
|
||||||
/// that elements can be expanded into multiple variables without breaking
|
/// that elements can be expanded into multiple variables without breaking
|
||||||
/// variables that appear after them.
|
/// variables that appear after them.
|
||||||
List<FoundElement> extractElements(AnalysisContext ctx,
|
List<FoundElement> extractElements({
|
||||||
{RequiredVariables required = RequiredVariables.empty}) {
|
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
|
// this contains variable references. For instance, SELECT :a = :a would
|
||||||
// contain two entries, both referring to the same variable. To do that,
|
// contain two entries, both referring to the same variable. To do that,
|
||||||
// we use the fact that each variable has a unique index.
|
// we use the fact that each variable has a unique index.
|
||||||
final variables = ctx.root.allDescendants.whereType<Variable>().toList();
|
final variables = collector.variables;
|
||||||
final placeholders =
|
final placeholders = collector.dartPlaceholders;
|
||||||
ctx.root.allDescendants.whereType<DartPlaceholder>().toList();
|
|
||||||
|
|
||||||
final merged = _mergeVarsAndPlaceholders(variables, placeholders);
|
final merged = _mergeVarsAndPlaceholders(variables, placeholders);
|
||||||
|
|
||||||
|
@ -152,6 +157,20 @@ class TypeMapper {
|
||||||
(used is NumberedVariable) ? used.explicitIndex : null;
|
(used is NumberedVariable) ? used.explicitIndex : null;
|
||||||
final internalType = ctx.typeOf(used);
|
final internalType = ctx.typeOf(used);
|
||||||
final type = resolvedToMoor(internalType.type);
|
final type = resolvedToMoor(internalType.type);
|
||||||
|
final forCapture = used.meta<CapturedVariable>();
|
||||||
|
|
||||||
|
if (forCapture != null) {
|
||||||
|
foundElements.add(FoundVariable.nestedQuery(
|
||||||
|
index: currentIndex,
|
||||||
|
name: name,
|
||||||
|
type: type,
|
||||||
|
variable: used,
|
||||||
|
forCaptured: forCapture,
|
||||||
|
));
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
final isArray = internalType.type?.isArray ?? false;
|
final isArray = internalType.type?.isArray ?? false;
|
||||||
final isRequired = required.requiredNamedVariables.contains(name) ||
|
final isRequired = required.requiredNamedVariables.contains(name) ||
|
||||||
required.requiredNumberedVariables.contains(used.resolvedIndex);
|
required.requiredNumberedVariables.contains(used.resolvedIndex);
|
||||||
|
@ -340,3 +359,46 @@ class TypeMapper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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 visitMoorSpecificNode(MoorSpecificNode 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.visitMoorSpecificNode(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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import 'package:build/build.dart';
|
import 'package:build/build.dart';
|
||||||
import 'package:drift_dev/moor_generator.dart';
|
|
||||||
import 'package:drift_dev/src/backends/build/moor_builder.dart';
|
import 'package:drift_dev/src/backends/build/moor_builder.dart';
|
||||||
import 'package:drift_dev/src/utils/type_utils.dart';
|
import 'package:drift_dev/src/utils/type_utils.dart';
|
||||||
import 'package:drift_dev/writer.dart';
|
import 'package:drift_dev/writer.dart';
|
||||||
|
@ -32,9 +31,8 @@ class DaoGenerator extends Generator implements BaseGenerator {
|
||||||
'$infoType get $getterName => attachedDatabase.$getterName;\n');
|
'$infoType get $getterName => attachedDatabase.$getterName;\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
for (final query in dao.queries ?? const <SqlQuery>[]) {
|
dao.queries
|
||||||
QueryWriter(query, classScope.child()).write();
|
?.forEach((query) => QueryWriter(classScope.child()).write(query));
|
||||||
}
|
|
||||||
|
|
||||||
classScope.leaf().write('}');
|
classScope.leaf().write('}');
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import 'package:collection/collection.dart';
|
||||||
import 'package:drift/drift.dart' show UpdateKind;
|
import 'package:drift/drift.dart' show UpdateKind;
|
||||||
import 'package:drift_dev/src/analyzer/options.dart';
|
import 'package:drift_dev/src/analyzer/options.dart';
|
||||||
import 'package:drift_dev/src/analyzer/runner/results.dart';
|
import 'package:drift_dev/src/analyzer/runner/results.dart';
|
||||||
|
import 'package:drift_dev/src/analyzer/sql_queries/nested_queries.dart';
|
||||||
import 'package:drift_dev/src/model/base_entity.dart';
|
import 'package:drift_dev/src/model/base_entity.dart';
|
||||||
import 'package:drift_dev/src/writer/writer.dart';
|
import 'package:drift_dev/src/writer/writer.dart';
|
||||||
import 'package:recase/recase.dart';
|
import 'package:recase/recase.dart';
|
||||||
|
@ -68,6 +69,7 @@ abstract class SqlQuery {
|
||||||
final String name;
|
final String name;
|
||||||
|
|
||||||
AnalysisContext? get fromContext;
|
AnalysisContext? get fromContext;
|
||||||
|
AstNode? get root;
|
||||||
List<AnalysisError>? lints;
|
List<AnalysisError>? lints;
|
||||||
|
|
||||||
/// Whether this query was declared in a `.moor` file.
|
/// Whether this query was declared in a `.moor` file.
|
||||||
|
@ -157,6 +159,28 @@ abstract class SqlQuery {
|
||||||
|
|
||||||
return resultClassName;
|
return resultClassName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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 {
|
class SqlSelectQuery extends SqlQuery {
|
||||||
|
@ -165,18 +189,28 @@ class SqlSelectQuery extends SqlQuery {
|
||||||
final InferredResultSet resultSet;
|
final InferredResultSet resultSet;
|
||||||
@override
|
@override
|
||||||
final AnalysisContext fromContext;
|
final AnalysisContext fromContext;
|
||||||
|
@override
|
||||||
|
final AstNode root;
|
||||||
|
|
||||||
/// The name of the result class, as requested by the user.
|
/// The name of the result class, as requested by the user.
|
||||||
// todo: Allow custom result classes for RETURNING as well?
|
// todo: Allow custom result classes for RETURNING as well?
|
||||||
final String? requestedResultClass;
|
final String? requestedResultClass;
|
||||||
|
|
||||||
|
final NestedQueriesContainer? nestedContainer;
|
||||||
|
|
||||||
|
/// Whether this query contains nested queries or not
|
||||||
|
bool get hasNestedQuery =>
|
||||||
|
resultSet.nestedResults.any((e) => e is NestedResultQuery);
|
||||||
|
|
||||||
SqlSelectQuery(
|
SqlSelectQuery(
|
||||||
String name,
|
String name,
|
||||||
this.fromContext,
|
this.fromContext,
|
||||||
|
this.root,
|
||||||
List<FoundElement> elements,
|
List<FoundElement> elements,
|
||||||
this.readsFrom,
|
this.readsFrom,
|
||||||
this.resultSet,
|
this.resultSet,
|
||||||
this.requestedResultClass,
|
this.requestedResultClass,
|
||||||
|
this.nestedContainer,
|
||||||
) : super(name, elements, hasMultipleTables: readsFrom.length > 1);
|
) : super(name, elements, hasMultipleTables: readsFrom.length > 1);
|
||||||
|
|
||||||
Set<MoorTable> get readsFromTables {
|
Set<MoorTable> get readsFromTables {
|
||||||
|
@ -196,14 +230,76 @@ class SqlSelectQuery extends SqlQuery {
|
||||||
return SqlSelectQuery(
|
return SqlSelectQuery(
|
||||||
name,
|
name,
|
||||||
fromContext,
|
fromContext,
|
||||||
|
root,
|
||||||
elements,
|
elements,
|
||||||
readsFrom,
|
readsFrom,
|
||||||
resultSet,
|
resultSet,
|
||||||
null,
|
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 UpdatingQuery extends SqlQuery {
|
class UpdatingQuery extends SqlQuery {
|
||||||
final List<WrittenMoorTable> updates;
|
final List<WrittenMoorTable> updates;
|
||||||
final bool isInsert;
|
final bool isInsert;
|
||||||
|
@ -211,6 +307,8 @@ class UpdatingQuery extends SqlQuery {
|
||||||
final InferredResultSet? resultSet;
|
final InferredResultSet? resultSet;
|
||||||
@override
|
@override
|
||||||
final AnalysisContext fromContext;
|
final AnalysisContext fromContext;
|
||||||
|
@override
|
||||||
|
final AstNode root;
|
||||||
|
|
||||||
bool get isOnlyDelete => updates.every((w) => w.kind == UpdateKind.delete);
|
bool get isOnlyDelete => updates.every((w) => w.kind == UpdateKind.delete);
|
||||||
|
|
||||||
|
@ -219,6 +317,7 @@ class UpdatingQuery extends SqlQuery {
|
||||||
UpdatingQuery(
|
UpdatingQuery(
|
||||||
String name,
|
String name,
|
||||||
this.fromContext,
|
this.fromContext,
|
||||||
|
this.root,
|
||||||
List<FoundElement> elements,
|
List<FoundElement> elements,
|
||||||
this.updates, {
|
this.updates, {
|
||||||
this.isInsert = false,
|
this.isInsert = false,
|
||||||
|
@ -239,6 +338,9 @@ class InTransactionQuery extends SqlQuery {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
AnalysisContext? get fromContext => null;
|
AnalysisContext? get fromContext => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
AstNode? get root => null;
|
||||||
}
|
}
|
||||||
|
|
||||||
class InferredResultSet {
|
class InferredResultSet {
|
||||||
|
@ -249,9 +351,9 @@ class InferredResultSet {
|
||||||
|
|
||||||
/// Tables in the result set that should appear as a class.
|
/// Tables in the result set that should appear as a class.
|
||||||
///
|
///
|
||||||
/// See [NestedResultTable] for further discussion and examples.
|
/// See [NestedResult] for further discussion and examples.
|
||||||
final List<NestedResultTable> nestedResults;
|
final List<NestedResult> nestedResults;
|
||||||
Map<NestedResultTable, String>? _expandedNestedPrefixes;
|
Map<NestedResult, String>? _expandedNestedPrefixes;
|
||||||
|
|
||||||
final List<ResultColumn> columns;
|
final List<ResultColumn> columns;
|
||||||
final Map<ResultColumn, String> _dartNames = {};
|
final Map<ResultColumn, String> _dartNames = {};
|
||||||
|
@ -293,7 +395,7 @@ class InferredResultSet {
|
||||||
bool get singleColumn =>
|
bool get singleColumn =>
|
||||||
matchingTable == null && nestedResults.isEmpty && columns.length == 1;
|
matchingTable == null && nestedResults.isEmpty && columns.length == 1;
|
||||||
|
|
||||||
String? nestedPrefixFor(NestedResultTable table) {
|
String? nestedPrefixFor(NestedResult table) {
|
||||||
if (_expandedNestedPrefixes == null) {
|
if (_expandedNestedPrefixes == null) {
|
||||||
var index = 0;
|
var index = 0;
|
||||||
_expandedNestedPrefixes = {
|
_expandedNestedPrefixes = {
|
||||||
|
@ -329,12 +431,17 @@ class InferredResultSet {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// [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
|
/// Checks whether this and the [other] result set have the same columns and
|
||||||
/// nested result sets.
|
/// nested result sets.
|
||||||
bool isCompatibleTo(InferredResultSet other) {
|
bool isCompatibleTo(InferredResultSet other) {
|
||||||
const columnsEquality = UnorderedIterableEquality(_ResultColumnEquality());
|
const columnsEquality = UnorderedIterableEquality(_ResultColumnEquality());
|
||||||
const nestedEquality =
|
const nestedEquality = UnorderedIterableEquality(_NestedResultEquality());
|
||||||
UnorderedIterableEquality(_NestedResultTableEquality());
|
|
||||||
|
|
||||||
return columnsEquality.equals(columns, other.columns) &&
|
return columnsEquality.equals(columns, other.columns) &&
|
||||||
nestedEquality.equals(nestedResults, other.nestedResults);
|
nestedEquality.equals(nestedResults, other.nestedResults);
|
||||||
|
@ -406,6 +513,15 @@ class ResultColumn implements HasType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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.
|
/// A nested table extracted from a `**` column.
|
||||||
///
|
///
|
||||||
/// For instance, consider this query:
|
/// For instance, consider this query:
|
||||||
|
@ -432,7 +548,7 @@ class ResultColumn implements HasType {
|
||||||
///
|
///
|
||||||
/// Knowing that `User` should be extracted into a field is represented with a
|
/// Knowing that `User` should be extracted into a field is represented with a
|
||||||
/// [NestedResultTable] information as part of the result set.
|
/// [NestedResultTable] information as part of the result set.
|
||||||
class NestedResultTable {
|
class NestedResultTable extends NestedResult {
|
||||||
final bool isNullable;
|
final bool isNullable;
|
||||||
final NestedStarResultColumn from;
|
final NestedStarResultColumn from;
|
||||||
final String name;
|
final String name;
|
||||||
|
@ -443,19 +559,54 @@ class NestedResultTable {
|
||||||
String get dartFieldName => ReCase(name).camelCase;
|
String get dartFieldName => ReCase(name).camelCase;
|
||||||
|
|
||||||
/// [hashCode] that matches [isCompatibleTo] instead of `==`.
|
/// [hashCode] that matches [isCompatibleTo] instead of `==`.
|
||||||
|
@override
|
||||||
int get compatibilityHashCode {
|
int get compatibilityHashCode {
|
||||||
return Object.hash(name, table);
|
return Object.hash(name, table);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks whether this is compatible to the [other] nested result, which is
|
/// 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.
|
/// the case iff they have the same and read from the same table.
|
||||||
bool isCompatibleTo(NestedResultTable other) {
|
@override
|
||||||
|
bool isCompatibleTo(NestedResult other) {
|
||||||
|
if (other is! NestedResultTable) return false;
|
||||||
|
|
||||||
return other.name == name &&
|
return other.name == name &&
|
||||||
other.table == table &&
|
other.table == table &&
|
||||||
other.isNullable == isNullable;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
String resultTypeCode() => query.resultTypeCode();
|
||||||
|
|
||||||
|
// 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,
|
/// Something in the query that needs special attention when generating code,
|
||||||
/// such as variables or Dart placeholders.
|
/// such as variables or Dart placeholders.
|
||||||
abstract class FoundElement {
|
abstract class FoundElement {
|
||||||
|
@ -466,6 +617,9 @@ abstract class FoundElement {
|
||||||
|
|
||||||
bool get hasSqlName => name != null;
|
bool get hasSqlName => name != null;
|
||||||
|
|
||||||
|
/// If the element should be hidden from the parameter list
|
||||||
|
bool get hidden => false;
|
||||||
|
|
||||||
/// Dart code for a type representing tis element.
|
/// Dart code for a type representing tis element.
|
||||||
String dartTypeCode([GenerationOptions options = const GenerationOptions()]);
|
String dartTypeCode([GenerationOptions options = const GenerationOptions()]);
|
||||||
}
|
}
|
||||||
|
@ -508,6 +662,13 @@ class FoundVariable extends FoundElement implements HasType {
|
||||||
|
|
||||||
final bool isRequired;
|
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({
|
FoundVariable({
|
||||||
required this.index,
|
required this.index,
|
||||||
required this.name,
|
required this.name,
|
||||||
|
@ -517,7 +678,21 @@ class FoundVariable extends FoundElement implements HasType {
|
||||||
this.isArray = false,
|
this.isArray = false,
|
||||||
this.isRequired = false,
|
this.isRequired = false,
|
||||||
this.typeConverter,
|
this.typeConverter,
|
||||||
}) : assert(variable.resolvedIndex == index);
|
}) : hidden = false,
|
||||||
|
forCaptured = null,
|
||||||
|
assert(variable.resolvedIndex == index);
|
||||||
|
|
||||||
|
FoundVariable.nestedQuery({
|
||||||
|
required this.index,
|
||||||
|
required this.name,
|
||||||
|
required this.type,
|
||||||
|
required this.variable,
|
||||||
|
required this.forCaptured,
|
||||||
|
}) : typeConverter = null,
|
||||||
|
nullable = false,
|
||||||
|
isArray = false,
|
||||||
|
isRequired = true,
|
||||||
|
hidden = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get dartParameterName {
|
String get dartParameterName {
|
||||||
|
@ -742,16 +917,16 @@ class _ResultColumnEquality implements Equality<ResultColumn> {
|
||||||
bool isValidKey(Object? e) => e is ResultColumn;
|
bool isValidKey(Object? e) => e is ResultColumn;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _NestedResultTableEquality implements Equality<NestedResultTable> {
|
class _NestedResultEquality implements Equality<NestedResult> {
|
||||||
const _NestedResultTableEquality();
|
const _NestedResultEquality();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool equals(NestedResultTable e1, NestedResultTable e2) {
|
bool equals(NestedResult e1, NestedResult e2) {
|
||||||
return e1.isCompatibleTo(e2);
|
return e1.isCompatibleTo(e2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int hash(NestedResultTable e) => e.compatibilityHashCode;
|
int hash(NestedResult e) => e.compatibilityHashCode;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool isValidKey(Object? e) => e is NestedResultTable;
|
bool isValidKey(Object? e) => e is NestedResultTable;
|
||||||
|
|
|
@ -44,7 +44,7 @@ class MoorTable extends MoorEntityWithResultSet {
|
||||||
@override
|
@override
|
||||||
final List<MoorColumn> columns;
|
final List<MoorColumn> columns;
|
||||||
|
|
||||||
/// The name of this table when stored in the database
|
/// The (unescaped) name of this table when stored in the database
|
||||||
final String sqlName;
|
final String sqlName;
|
||||||
|
|
||||||
/// The name for the data class associated with this table
|
/// The name for the data class associated with this table
|
||||||
|
|
|
@ -117,9 +117,7 @@ class DatabaseWriter {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write implementation for query methods
|
// Write implementation for query methods
|
||||||
for (final query in db.queries ?? const <Never>[]) {
|
db.queries?.forEach((query) => QueryWriter(dbScope.child()).write(query));
|
||||||
QueryWriter(query, dbScope.child()).write();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write List of tables
|
// Write List of tables
|
||||||
final schemaScope = dbScope.leaf();
|
final schemaScope = dbScope.leaf();
|
||||||
|
|
|
@ -3,6 +3,7 @@ import 'dart:math' show max;
|
||||||
import 'package:drift_dev/moor_generator.dart';
|
import 'package:drift_dev/moor_generator.dart';
|
||||||
import 'package:drift_dev/src/analyzer/options.dart';
|
import 'package:drift_dev/src/analyzer/options.dart';
|
||||||
import 'package:drift_dev/src/analyzer/sql_queries/explicit_alias_transformer.dart';
|
import 'package:drift_dev/src/analyzer/sql_queries/explicit_alias_transformer.dart';
|
||||||
|
import 'package:drift_dev/src/analyzer/sql_queries/nested_queries.dart';
|
||||||
import 'package:drift_dev/src/utils/string_escaper.dart';
|
import 'package:drift_dev/src/utils/string_escaper.dart';
|
||||||
import 'package:drift_dev/writer.dart';
|
import 'package:drift_dev/writer.dart';
|
||||||
import 'package:recase/recase.dart';
|
import 'package:recase/recase.dart';
|
||||||
|
@ -18,25 +19,16 @@ int _compareNodes(AstNode a, AstNode b) =>
|
||||||
/// Writes the handling code for a query. The code emitted will be a method that
|
/// Writes the handling code for a query. The code emitted will be a method that
|
||||||
/// should be included in a generated database or dao class.
|
/// should be included in a generated database or dao class.
|
||||||
class QueryWriter {
|
class QueryWriter {
|
||||||
final SqlQuery query;
|
|
||||||
final Scope scope;
|
final Scope scope;
|
||||||
|
|
||||||
late ExplicitAliasTransformer _transformer;
|
late final ExplicitAliasTransformer _transformer;
|
||||||
|
final StringBuffer _buffer;
|
||||||
SqlSelectQuery get _select => query as SqlSelectQuery;
|
|
||||||
UpdatingQuery get _update => query as UpdatingQuery;
|
|
||||||
|
|
||||||
MoorOptions get options => scope.writer.options;
|
MoorOptions get options => scope.writer.options;
|
||||||
late StringBuffer _buffer;
|
|
||||||
|
|
||||||
bool get _newSelectableMode =>
|
QueryWriter(this.scope) : _buffer = scope.leaf();
|
||||||
query.declaredInMoorFile || options.compactQueryMethods;
|
|
||||||
|
|
||||||
QueryWriter(this.query, this.scope) {
|
void write(SqlQuery query) {
|
||||||
_buffer = scope.leaf();
|
|
||||||
}
|
|
||||||
|
|
||||||
void write() {
|
|
||||||
// Note that writing queries can have a result set if they use a RETURNING
|
// Note that writing queries can have a result set if they use a RETURNING
|
||||||
// clause.
|
// clause.
|
||||||
final resultSet = query.resultSet;
|
final resultSet = query.resultSet;
|
||||||
|
@ -54,40 +46,52 @@ class QueryWriter {
|
||||||
// analysis, Dart getter names stay the same.
|
// analysis, Dart getter names stay the same.
|
||||||
if (resultSet != null && options.newSqlCodeGeneration) {
|
if (resultSet != null && options.newSqlCodeGeneration) {
|
||||||
_transformer = ExplicitAliasTransformer();
|
_transformer = ExplicitAliasTransformer();
|
||||||
_transformer.rewrite(query.fromContext!.root);
|
_transformer.rewrite(query.root!);
|
||||||
|
|
||||||
|
final nested = query is SqlSelectQuery ? query.nestedContainer : null;
|
||||||
|
if (nested != null) {
|
||||||
|
addHelperNodes(nested);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query is SqlSelectQuery) {
|
if (query is SqlSelectQuery) {
|
||||||
_writeSelect();
|
_writeSelect(query);
|
||||||
} else if (query is UpdatingQuery) {
|
} else if (query is UpdatingQuery) {
|
||||||
if (resultSet != null) {
|
if (resultSet != null) {
|
||||||
_writeUpdatingQueryWithReturning();
|
_writeUpdatingQueryWithReturning(query);
|
||||||
} else {
|
} else {
|
||||||
_writeUpdatingQuery();
|
_writeUpdatingQuery(query);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _writeSelect() {
|
void _writeSelect(SqlSelectQuery select) {
|
||||||
_writeSelectStatementCreator();
|
if (select.hasNestedQuery && !scope.options.newSqlCodeGeneration) {
|
||||||
|
throw UnsupportedError(
|
||||||
|
'Using nested result queries (with `LIST`) requires the '
|
||||||
|
'`new_sql_code_generation` build option.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!_newSelectableMode) {
|
_writeSelectStatementCreator(select);
|
||||||
_writeOneTimeReader();
|
|
||||||
_writeStreamReader();
|
if (!select.declaredInMoorFile && !options.compactQueryMethods) {
|
||||||
|
_writeOneTimeReader(select);
|
||||||
|
_writeStreamReader(select);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _nameOfCreationMethod() {
|
String _nameOfCreationMethod(SqlSelectQuery select) {
|
||||||
if (_newSelectableMode) {
|
if (!select.declaredInMoorFile && !options.compactQueryMethods) {
|
||||||
return query.name;
|
return '${select.name}Query';
|
||||||
} else {
|
} else {
|
||||||
return '${query.name}Query';
|
return select.name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Writes the function literal that turns a "QueryRow" into the desired
|
/// Writes the function literal that turns a "QueryRow" into the desired
|
||||||
/// custom return type of a query.
|
/// custom return type of a query.
|
||||||
void _writeMappingLambda() {
|
void _writeMappingLambda(SqlQuery query) {
|
||||||
final resultSet = query.resultSet!;
|
final resultSet = query.resultSet!;
|
||||||
|
|
||||||
if (resultSet.singleColumn) {
|
if (resultSet.singleColumn) {
|
||||||
|
@ -119,7 +123,11 @@ class QueryWriter {
|
||||||
_buffer.write('})');
|
_buffer.write('})');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_buffer.write('(QueryRow row) { return ${query.resultClassName}(');
|
_buffer.write('(QueryRow row) ');
|
||||||
|
if (query is SqlSelectQuery && query.hasNestedQuery) {
|
||||||
|
_buffer.write('async ');
|
||||||
|
}
|
||||||
|
_buffer.write('{ return ${query.resultClassName}(');
|
||||||
|
|
||||||
if (options.rawResultSetData) {
|
if (options.rawResultSetData) {
|
||||||
_buffer.write('row: row,\n');
|
_buffer.write('row: row,\n');
|
||||||
|
@ -131,17 +139,26 @@ class QueryWriter {
|
||||||
'${readingCode(column, scope.generationOptions, options)},');
|
'${readingCode(column, scope.generationOptions, options)},');
|
||||||
}
|
}
|
||||||
for (final nested in resultSet.nestedResults) {
|
for (final nested in resultSet.nestedResults) {
|
||||||
final prefix = resultSet.nestedPrefixFor(nested);
|
if (nested is NestedResultTable) {
|
||||||
if (prefix == null) continue;
|
final prefix = resultSet.nestedPrefixFor(nested);
|
||||||
|
if (prefix == null) continue;
|
||||||
|
|
||||||
final fieldName = nested.dartFieldName;
|
final fieldName = nested.dartFieldName;
|
||||||
final tableGetter = nested.table.dbGetterName;
|
final tableGetter = nested.table.dbGetterName;
|
||||||
|
|
||||||
final mappingMethod =
|
final mappingMethod =
|
||||||
nested.isNullable ? 'mapFromRowOrNull' : 'mapFromRow';
|
nested.isNullable ? 'mapFromRowOrNull' : 'mapFromRow';
|
||||||
|
|
||||||
_buffer.write('$fieldName: $tableGetter.$mappingMethod(row, '
|
_buffer.write('$fieldName: $tableGetter.$mappingMethod(row, '
|
||||||
'tablePrefix: ${asDartLiteral(prefix)}),');
|
'tablePrefix: ${asDartLiteral(prefix)}),');
|
||||||
|
} else if (nested is NestedResultQuery) {
|
||||||
|
final fieldName = nested.filedName();
|
||||||
|
_buffer.write('$fieldName: await ');
|
||||||
|
|
||||||
|
_writeCustomSelectStatement(nested.query);
|
||||||
|
|
||||||
|
_buffer.write('.get(),');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_buffer.write(');\n}');
|
_buffer.write(');\n}');
|
||||||
}
|
}
|
||||||
|
@ -180,101 +197,111 @@ class QueryWriter {
|
||||||
|
|
||||||
/// Writes a method returning a `Selectable<T>`, where `T` is the return type
|
/// Writes a method returning a `Selectable<T>`, where `T` is the return type
|
||||||
/// of the custom query.
|
/// of the custom query.
|
||||||
void _writeSelectStatementCreator() {
|
void _writeSelectStatementCreator(SqlSelectQuery select) {
|
||||||
final returnType =
|
final returnType =
|
||||||
'Selectable<${_select.resultTypeCode(scope.generationOptions)}>';
|
'Selectable<${select.resultTypeCode(scope.generationOptions)}>';
|
||||||
final methodName = _nameOfCreationMethod();
|
final methodName = _nameOfCreationMethod(select);
|
||||||
|
|
||||||
_buffer.write('$returnType $methodName(');
|
_buffer.write('$returnType $methodName(');
|
||||||
_writeParameters();
|
_writeParameters(select);
|
||||||
_buffer.write(') {\n');
|
_buffer.write(') {\n');
|
||||||
|
|
||||||
_writeExpandedDeclarations();
|
_writeExpandedDeclarations(select);
|
||||||
_buffer.write('return customSelect(${_queryCode()}, ');
|
_buffer.write('return');
|
||||||
_writeVariables();
|
_writeCustomSelectStatement(select);
|
||||||
_buffer.write(', ');
|
_buffer.write(';\n}\n');
|
||||||
_writeReadsFrom();
|
|
||||||
|
|
||||||
_buffer.write(').map(');
|
|
||||||
_writeMappingLambda();
|
|
||||||
_buffer.write(');\n}\n');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _writeOneTimeReader() {
|
void _writeCustomSelectStatement(SqlSelectQuery select) {
|
||||||
|
_buffer.write(' customSelect(${_queryCode(select)}, ');
|
||||||
|
_writeVariables(select);
|
||||||
|
_buffer.write(', ');
|
||||||
|
_writeReadsFrom(select);
|
||||||
|
|
||||||
|
if (select.hasNestedQuery) {
|
||||||
|
_buffer.write(').asyncMap(');
|
||||||
|
} else {
|
||||||
|
_buffer.write(').map(');
|
||||||
|
}
|
||||||
|
_writeMappingLambda(select);
|
||||||
|
_buffer.write(')');
|
||||||
|
}
|
||||||
|
|
||||||
|
void _writeOneTimeReader(SqlSelectQuery select) {
|
||||||
final returnType =
|
final returnType =
|
||||||
'Future<List<${_select.resultTypeCode(scope.generationOptions)}>>';
|
'Future<List<${select.resultTypeCode(scope.generationOptions)}>>';
|
||||||
_buffer.write('$returnType ${query.name}(');
|
_buffer.write('$returnType ${select.name}(');
|
||||||
_writeParameters();
|
_writeParameters(select);
|
||||||
_buffer
|
_buffer
|
||||||
..write(') {\n')
|
..write(') {\n')
|
||||||
..write('return ${_nameOfCreationMethod()}(');
|
..write('return ${_nameOfCreationMethod(select)}(');
|
||||||
_writeUseParameters();
|
_writeUseParameters(select);
|
||||||
_buffer.write(').get();\n}\n');
|
_buffer.write(').get();\n}\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
void _writeStreamReader() {
|
void _writeStreamReader(SqlSelectQuery select) {
|
||||||
final upperQueryName = ReCase(query.name).pascalCase;
|
final upperQueryName = ReCase(select.name).pascalCase;
|
||||||
|
|
||||||
String methodName;
|
String methodName;
|
||||||
// turning the query name into pascal case will remove underscores, add the
|
// turning the query name into pascal case will remove underscores, add the
|
||||||
// "private" modifier back in
|
// "private" modifier back in
|
||||||
if (query.name.startsWith('_')) {
|
if (select.name.startsWith('_')) {
|
||||||
methodName = '_watch$upperQueryName';
|
methodName = '_watch$upperQueryName';
|
||||||
} else {
|
} else {
|
||||||
methodName = 'watch$upperQueryName';
|
methodName = 'watch$upperQueryName';
|
||||||
}
|
}
|
||||||
|
|
||||||
final returnType =
|
final returnType =
|
||||||
'Stream<List<${_select.resultTypeCode(scope.generationOptions)}>>';
|
'Stream<List<${select.resultTypeCode(scope.generationOptions)}>>';
|
||||||
_buffer.write('$returnType $methodName(');
|
_buffer.write('$returnType $methodName(');
|
||||||
_writeParameters();
|
_writeParameters(select);
|
||||||
_buffer
|
_buffer
|
||||||
..write(') {\n')
|
..write(') {\n')
|
||||||
..write('return ${_nameOfCreationMethod()}(');
|
..write('return ${_nameOfCreationMethod(select)}(');
|
||||||
_writeUseParameters();
|
_writeUseParameters(select);
|
||||||
_buffer.write(').watch();\n}\n');
|
_buffer.write(').watch();\n}\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
void _writeUpdatingQueryWithReturning() {
|
void _writeUpdatingQueryWithReturning(UpdatingQuery update) {
|
||||||
final type = query.resultTypeCode(scope.generationOptions);
|
final type = update.resultTypeCode(scope.generationOptions);
|
||||||
_buffer.write('Future<List<$type>> ${query.name}(');
|
_buffer.write('Future<List<$type>> ${update.name}(');
|
||||||
_writeParameters();
|
_writeParameters(update);
|
||||||
_buffer.write(') {\n');
|
_buffer.write(') {\n');
|
||||||
|
|
||||||
_writeExpandedDeclarations();
|
_writeExpandedDeclarations(update);
|
||||||
_buffer.write('return customWriteReturning(${_queryCode()},');
|
_buffer.write('return customWriteReturning(${_queryCode(update)},');
|
||||||
_writeCommonUpdateParameters();
|
_writeCommonUpdateParameters(update);
|
||||||
_buffer.write(').then((rows) => rows.map(');
|
_buffer.write(').then((rows) => rows.map(');
|
||||||
_writeMappingLambda();
|
_writeMappingLambda(update);
|
||||||
_buffer.write(').toList());\n}');
|
_buffer.write(').toList());\n}');
|
||||||
}
|
}
|
||||||
|
|
||||||
void _writeUpdatingQuery() {
|
void _writeUpdatingQuery(UpdatingQuery update) {
|
||||||
/*
|
/*
|
||||||
Future<int> test() {
|
Future<int> test() {
|
||||||
return customUpdate('', variables: [], updates: {});
|
return customUpdate('', variables: [], updates: {});
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
final implName = _update.isInsert ? 'customInsert' : 'customUpdate';
|
final implName = update.isInsert ? 'customInsert' : 'customUpdate';
|
||||||
|
|
||||||
_buffer.write('Future<int> ${query.name}(');
|
_buffer.write('Future<int> ${update.name}(');
|
||||||
_writeParameters();
|
_writeParameters(update);
|
||||||
_buffer.write(') {\n');
|
_buffer.write(') {\n');
|
||||||
|
|
||||||
_writeExpandedDeclarations();
|
_writeExpandedDeclarations(update);
|
||||||
_buffer.write('return $implName(${_queryCode()},');
|
_buffer.write('return $implName(${_queryCode(update)},');
|
||||||
_writeCommonUpdateParameters();
|
_writeCommonUpdateParameters(update);
|
||||||
_buffer.write(',);\n}\n');
|
_buffer.write(',);\n}\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
void _writeCommonUpdateParameters() {
|
void _writeCommonUpdateParameters(UpdatingQuery update) {
|
||||||
_writeVariables();
|
_writeVariables(update);
|
||||||
_buffer.write(',');
|
_buffer.write(',');
|
||||||
_writeUpdates();
|
_writeUpdates(update);
|
||||||
_writeUpdateKind();
|
_writeUpdateKind(update);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _writeParameters() {
|
void _writeParameters(SqlQuery query) {
|
||||||
final namedElements = <FoundElement>[];
|
final namedElements = <FoundElement>[];
|
||||||
|
|
||||||
String typeFor(FoundElement element) {
|
String typeFor(FoundElement element) {
|
||||||
|
@ -294,7 +321,9 @@ class QueryWriter {
|
||||||
}
|
}
|
||||||
|
|
||||||
var needsComma = false;
|
var needsComma = false;
|
||||||
for (final element in query.elements) {
|
for (final element in query.elementsWithNestedQueries()) {
|
||||||
|
if (element.hidden) continue;
|
||||||
|
|
||||||
// Placeholders with a default value generate optional (and thus, named)
|
// Placeholders with a default value generate optional (and thus, named)
|
||||||
// parameters. Since moor 4, we have an option to also generate named
|
// parameters. Since moor 4, we have an option to also generate named
|
||||||
// parameters for named variables.
|
// parameters for named variables.
|
||||||
|
@ -407,35 +436,37 @@ class QueryWriter {
|
||||||
/// Writes code that uses the parameters as declared by [_writeParameters],
|
/// Writes code that uses the parameters as declared by [_writeParameters],
|
||||||
/// assuming that for each parameter, a variable with the same name exists
|
/// assuming that for each parameter, a variable with the same name exists
|
||||||
/// in the current scope.
|
/// in the current scope.
|
||||||
void _writeUseParameters() {
|
void _writeUseParameters(SqlQuery query) {
|
||||||
final parameters = query.elements.map((e) => e.dartParameterName);
|
final parameters = query.elements.map((e) => e.dartParameterName);
|
||||||
_buffer.write(parameters.join(', '));
|
_buffer.write(parameters.join(', '));
|
||||||
}
|
}
|
||||||
|
|
||||||
void _writeExpandedDeclarations() {
|
void _writeExpandedDeclarations(SqlQuery query) {
|
||||||
_ExpandedDeclarationWriter(query, options, _buffer)
|
_ExpandedDeclarationWriter(query, options, _buffer)
|
||||||
.writeExpandedDeclarations();
|
.writeExpandedDeclarations();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _writeVariables() {
|
void _writeVariables(SqlQuery query) {
|
||||||
_ExpandedVariableWriter(query, scope, _buffer).writeVariables();
|
_ExpandedVariableWriter(query, scope, _buffer).writeVariables();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a Dart string literal representing the query after variables have
|
/// Returns a Dart string literal representing the query after variables have
|
||||||
/// been expanded. For instance, 'SELECT * FROM t WHERE x IN ?' will be turned
|
/// been expanded. For instance, 'SELECT * FROM t WHERE x IN ?' will be turned
|
||||||
/// into 'SELECT * FROM t WHERE x IN ($expandedVar1)'.
|
/// into 'SELECT * FROM t WHERE x IN ($expandedVar1)'.
|
||||||
String _queryCode() {
|
String _queryCode(SqlQuery query) {
|
||||||
if (scope.options.newSqlCodeGeneration) {
|
if (scope.options.newSqlCodeGeneration) {
|
||||||
return SqlWriter(scope.options, query: query).write();
|
return SqlWriter(scope.options, query: query).write();
|
||||||
} else {
|
} else {
|
||||||
return _legacyQueryCode();
|
return _legacyQueryCode(query);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _legacyQueryCode() {
|
String _legacyQueryCode(SqlQuery query) {
|
||||||
final context = query.fromContext!;
|
final root = query.root!;
|
||||||
|
final sql = query.fromContext!.sql;
|
||||||
|
|
||||||
// sort variables and placeholders by the order in which they appear
|
// sort variables and placeholders by the order in which they appear
|
||||||
final toReplace = context.root.allDescendants
|
final toReplace = root.allDescendants
|
||||||
.where((node) =>
|
.where((node) =>
|
||||||
node is Variable ||
|
node is Variable ||
|
||||||
node is DartPlaceholder ||
|
node is DartPlaceholder ||
|
||||||
|
@ -450,17 +481,17 @@ class QueryWriter {
|
||||||
const <NestedStarResultColumn, NestedResultTable>{};
|
const <NestedStarResultColumn, NestedResultTable>{};
|
||||||
if (query is SqlSelectQuery) {
|
if (query is SqlSelectQuery) {
|
||||||
doubleStarColumnToResolvedTable = {
|
doubleStarColumnToResolvedTable = {
|
||||||
for (final nestedResult in _select.resultSet.nestedResults)
|
for (final nestedResult in query.resultSet.nestedResults)
|
||||||
nestedResult.from: nestedResult
|
if (nestedResult is NestedResultTable) nestedResult.from: nestedResult
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
var lastIndex = context.root.firstPosition;
|
var lastIndex = root.firstPosition;
|
||||||
|
|
||||||
void replaceNode(AstNode node, String content) {
|
void replaceNode(AstNode node, String content) {
|
||||||
// write everything that comes before this var into the buffer
|
// write everything that comes before this var into the buffer
|
||||||
final currentIndex = node.firstPosition;
|
final currentIndex = node.firstPosition;
|
||||||
final queryPart = context.sql.substring(lastIndex, currentIndex);
|
final queryPart = sql.substring(lastIndex, currentIndex);
|
||||||
buffer.write(escapeForDart(queryPart));
|
buffer.write(escapeForDart(queryPart));
|
||||||
lastIndex = node.lastPosition;
|
lastIndex = node.lastPosition;
|
||||||
|
|
||||||
|
@ -486,7 +517,9 @@ class QueryWriter {
|
||||||
final result = doubleStarColumnToResolvedTable[rewriteTarget];
|
final result = doubleStarColumnToResolvedTable[rewriteTarget];
|
||||||
if (result == null) continue;
|
if (result == null) continue;
|
||||||
|
|
||||||
final prefix = _select.resultSet.nestedPrefixFor(result);
|
// weird cast here :O
|
||||||
|
final prefix =
|
||||||
|
(query as SqlSelectQuery).resultSet.nestedPrefixFor(result);
|
||||||
final table = rewriteTarget.tableName;
|
final table = rewriteTarget.tableName;
|
||||||
|
|
||||||
// Convert foo.** to "foo.a" AS "nested_0.a", ... for all columns in foo
|
// Convert foo.** to "foo.a" AS "nested_0.a", ... for all columns in foo
|
||||||
|
@ -509,40 +542,42 @@ class QueryWriter {
|
||||||
}
|
}
|
||||||
|
|
||||||
// write the final part after the last variable, plus the ending '
|
// write the final part after the last variable, plus the ending '
|
||||||
final lastPosition = context.root.lastPosition;
|
final lastPosition = root.lastPosition;
|
||||||
buffer
|
buffer
|
||||||
..write(escapeForDart(context.sql.substring(lastIndex, lastPosition)))
|
..write(escapeForDart(sql.substring(lastIndex, lastPosition)))
|
||||||
..write("'");
|
..write("'");
|
||||||
|
|
||||||
return buffer.toString();
|
return buffer.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _writeReadsFrom() {
|
void _writeReadsFrom(SqlSelectQuery select) {
|
||||||
_buffer.write('readsFrom: {');
|
_buffer.write('readsFrom: {');
|
||||||
|
|
||||||
for (final table in _select.readsFromTables) {
|
for (final table in select.readsFromTables) {
|
||||||
_buffer.write('${table.dbGetterName},');
|
_buffer.write('${table.dbGetterName},');
|
||||||
}
|
}
|
||||||
|
|
||||||
for (final element in query.elements.whereType<FoundDartPlaceholder>()) {
|
for (final element in select
|
||||||
|
.elementsWithNestedQueries()
|
||||||
|
.whereType<FoundDartPlaceholder>()) {
|
||||||
_buffer.write('...${placeholderContextName(element)}.watchedTables,');
|
_buffer.write('...${placeholderContextName(element)}.watchedTables,');
|
||||||
}
|
}
|
||||||
|
|
||||||
_buffer.write('}');
|
_buffer.write('}');
|
||||||
}
|
}
|
||||||
|
|
||||||
void _writeUpdates() {
|
void _writeUpdates(UpdatingQuery update) {
|
||||||
final from = _update.updates.map((t) => t.table.dbGetterName).join(', ');
|
final from = update.updates.map((t) => t.table.dbGetterName).join(', ');
|
||||||
_buffer
|
_buffer
|
||||||
..write('updates: {')
|
..write('updates: {')
|
||||||
..write(from)
|
..write(from)
|
||||||
..write('}');
|
..write('}');
|
||||||
}
|
}
|
||||||
|
|
||||||
void _writeUpdateKind() {
|
void _writeUpdateKind(UpdatingQuery update) {
|
||||||
if (_update.isOnlyDelete) {
|
if (update.isOnlyDelete) {
|
||||||
_buffer.write(', updateKind: UpdateKind.delete');
|
_buffer.write(', updateKind: UpdateKind.delete');
|
||||||
} else if (_update.isOnlyUpdate) {
|
} else if (update.isOnlyUpdate) {
|
||||||
_buffer.write(', updateKind: UpdateKind.update');
|
_buffer.write(', updateKind: UpdateKind.update');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -656,7 +691,7 @@ class _ExpandedDeclarationWriter {
|
||||||
}
|
}
|
||||||
|
|
||||||
needsIndexCounter = true;
|
needsIndexCounter = true;
|
||||||
for (final element in query.elements) {
|
for (final element in query.elementsWithNestedQueries()) {
|
||||||
if (element is FoundVariable) {
|
if (element is FoundVariable) {
|
||||||
if (element.isArray) {
|
if (element.isArray) {
|
||||||
_writeArrayVariable(element);
|
_writeArrayVariable(element);
|
||||||
|
@ -830,6 +865,7 @@ class _ExpandedVariableWriter {
|
||||||
final type =
|
final type =
|
||||||
element.variableTypeCodeWithoutArray(scope.generationOptions);
|
element.variableTypeCodeWithoutArray(scope.generationOptions);
|
||||||
final buffer = StringBuffer('Variable<$type>(');
|
final buffer = StringBuffer('Variable<$type>(');
|
||||||
|
final capture = element.forCaptured;
|
||||||
|
|
||||||
if (element.typeConverter != null) {
|
if (element.typeConverter != null) {
|
||||||
// Apply the converter
|
// Apply the converter
|
||||||
|
@ -841,6 +877,8 @@ class _ExpandedVariableWriter {
|
||||||
if (needsNullAssertion) {
|
if (needsNullAssertion) {
|
||||||
buffer.write('!');
|
buffer.write('!');
|
||||||
}
|
}
|
||||||
|
} else if (capture != null) {
|
||||||
|
buffer.write('row.read(${asDartLiteral(capture.helperColumn)})');
|
||||||
} else {
|
} else {
|
||||||
buffer.write(dartExpr);
|
buffer.write(dartExpr);
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,17 +39,31 @@ class ResultSetWriter {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (final nested in resultSet.nestedResults) {
|
for (final nested in resultSet.nestedResults) {
|
||||||
var typeName = nested.table.dartTypeCode(scope.generationOptions);
|
if (nested is NestedResultTable) {
|
||||||
final fieldName = nested.dartFieldName;
|
var typeName = nested.table.dartTypeCode(scope.generationOptions);
|
||||||
|
final fieldName = nested.dartFieldName;
|
||||||
|
|
||||||
if (nested.isNullable) {
|
if (nested.isNullable) {
|
||||||
typeName = scope.nullableType(typeName);
|
typeName = scope.nullableType(typeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
into.write('$modifier $typeName $fieldName;\n');
|
||||||
|
|
||||||
|
fieldNames.add(fieldName);
|
||||||
|
if (!nested.isNullable) nonNullableFields.add(fieldName);
|
||||||
|
} else if (nested is NestedResultQuery) {
|
||||||
|
final fieldName = nested.filedName();
|
||||||
|
final typeName = nested.resultTypeCode();
|
||||||
|
|
||||||
|
if (nested.query.resultSet.needsOwnClass) {
|
||||||
|
ResultSetWriter(nested.query, scope).write();
|
||||||
|
}
|
||||||
|
|
||||||
|
into.write('$modifier List<$typeName> $fieldName;\n');
|
||||||
|
|
||||||
|
fieldNames.add(fieldName);
|
||||||
|
nonNullableFields.add(fieldName);
|
||||||
}
|
}
|
||||||
|
|
||||||
into.write('$modifier $typeName $fieldName;\n');
|
|
||||||
|
|
||||||
fieldNames.add(fieldName);
|
|
||||||
if (!nested.isNullable) nonNullableFields.add(fieldName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// write the constructor
|
// write the constructor
|
||||||
|
|
|
@ -46,7 +46,7 @@ class SqlWriter extends NodeSqlBuilder {
|
||||||
if (query is SqlSelectQuery) {
|
if (query is SqlSelectQuery) {
|
||||||
doubleStarColumnToResolvedTable = {
|
doubleStarColumnToResolvedTable = {
|
||||||
for (final nestedResult in query.resultSet.nestedResults)
|
for (final nestedResult in query.resultSet.nestedResults)
|
||||||
nestedResult.from: nestedResult
|
if (nestedResult is NestedResultTable) nestedResult.from: nestedResult
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return SqlWriter._(query, options, doubleStarColumnToResolvedTable,
|
return SqlWriter._(query, options, doubleStarColumnToResolvedTable,
|
||||||
|
@ -54,7 +54,7 @@ class SqlWriter extends NodeSqlBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
String write() {
|
String write() {
|
||||||
return writeNodeIntoStringLiteral(query!.fromContext!.root);
|
return writeNodeIntoStringLiteral(query!.root!);
|
||||||
}
|
}
|
||||||
|
|
||||||
String writeNodeIntoStringLiteral(AstNode node) {
|
String writeNodeIntoStringLiteral(AstNode node) {
|
||||||
|
@ -147,6 +147,13 @@ class SqlWriter extends NodeSqlBuilder {
|
||||||
query!.placeholders.singleWhere((p) => p.astNode == e);
|
query!.placeholders.singleWhere((p) => p.astNode == e);
|
||||||
|
|
||||||
_writeRawInSpaces('\${${placeholderContextName(moorPlaceholder)}.sql}');
|
_writeRawInSpaces('\${${placeholderContextName(moorPlaceholder)}.sql}');
|
||||||
|
} else if (e is NestedQueryColumn) {
|
||||||
|
assert(
|
||||||
|
false,
|
||||||
|
'This should be unreachable, because all NestedQueryColumns are '
|
||||||
|
'replaced in the NestedQueryTransformer with there required input '
|
||||||
|
'variables (or just removed if no variables are required)',
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return super.visitMoorSpecificNode(e, arg);
|
return super.visitMoorSpecificNode(e, arg);
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ void main() {
|
||||||
|
|
||||||
test('warns when a result column is unresolved', () {
|
test('warns when a result column is unresolved', () {
|
||||||
final result = engine.analyze('SELECT ?;');
|
final result = engine.analyze('SELECT ?;');
|
||||||
final moorQuery = QueryHandler(fakeQuery, result, mapper).handle();
|
final moorQuery = QueryHandler(result, mapper).handle(fakeQuery);
|
||||||
|
|
||||||
expect(moorQuery.lints,
|
expect(moorQuery.lints,
|
||||||
anyElement((AnalysisError q) => q.message.contains('unknown type')));
|
anyElement((AnalysisError q) => q.message.contains('unknown type')));
|
||||||
|
@ -25,7 +25,7 @@ void main() {
|
||||||
|
|
||||||
test('warns when the result depends on a Dart template', () {
|
test('warns when the result depends on a Dart template', () {
|
||||||
final result = engine.analyze(r"SELECT 'string' = $expr;");
|
final result = engine.analyze(r"SELECT 'string' = $expr;");
|
||||||
final moorQuery = QueryHandler(fakeQuery, result, mapper).handle();
|
final moorQuery = QueryHandler(result, mapper).handle(fakeQuery);
|
||||||
|
|
||||||
expect(moorQuery.lints,
|
expect(moorQuery.lints,
|
||||||
anyElement((AnalysisError q) => q.message.contains('Dart template')));
|
anyElement((AnalysisError q) => q.message.contains('Dart template')));
|
||||||
|
@ -33,7 +33,7 @@ void main() {
|
||||||
|
|
||||||
test('warns when nested results refer to table-valued functions', () {
|
test('warns when nested results refer to table-valued functions', () {
|
||||||
final result = engine.analyze("SELECT json_each.** FROM json_each('')");
|
final result = engine.analyze("SELECT json_each.** FROM json_each('')");
|
||||||
final moorQuery = QueryHandler(fakeQuery, result, mapper).handle();
|
final moorQuery = QueryHandler(result, mapper).handle(fakeQuery);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
moorQuery.lints,
|
moorQuery.lints,
|
||||||
|
@ -95,7 +95,7 @@ in: INSERT INTO foo (id) $placeholder;
|
||||||
group('warns about wrong types in subexpressions', () {
|
group('warns about wrong types in subexpressions', () {
|
||||||
test('strings in arithmetic', () {
|
test('strings in arithmetic', () {
|
||||||
final result = engine.analyze("SELECT 'foo' + 3;");
|
final result = engine.analyze("SELECT 'foo' + 3;");
|
||||||
final moorQuery = QueryHandler(fakeQuery, result, mapper).handle();
|
final moorQuery = QueryHandler(result, mapper).handle(fakeQuery);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
moorQuery.lints,
|
moorQuery.lints,
|
||||||
|
@ -106,14 +106,14 @@ in: INSERT INTO foo (id) $placeholder;
|
||||||
|
|
||||||
test('allows numerics in arithmetic', () {
|
test('allows numerics in arithmetic', () {
|
||||||
final result = engine.analyze('SELECT 3.6 * 3;');
|
final result = engine.analyze('SELECT 3.6 * 3;');
|
||||||
final moorQuery = QueryHandler(fakeQuery, result, mapper).handle();
|
final moorQuery = QueryHandler(result, mapper).handle(fakeQuery);
|
||||||
|
|
||||||
expect(moorQuery.lints, isEmpty);
|
expect(moorQuery.lints, isEmpty);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('real in binary', () {
|
test('real in binary', () {
|
||||||
final result = engine.analyze('SELECT 3.5 | 3;');
|
final result = engine.analyze('SELECT 3.5 | 3;');
|
||||||
final moorQuery = QueryHandler(fakeQuery, result, mapper).handle();
|
final moorQuery = QueryHandler(result, mapper).handle(fakeQuery);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
moorQuery.lints,
|
moorQuery.lints,
|
||||||
|
@ -123,9 +123,11 @@ in: INSERT INTO foo (id) $placeholder;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('warns when nested results appear in compound statements', () async {
|
test(
|
||||||
final state = TestState.withContent({
|
'warns when nested results appear in compound statements',
|
||||||
'foo|lib/a.moor': '''
|
() async {
|
||||||
|
final state = TestState.withContent({
|
||||||
|
'foo|lib/a.moor': '''
|
||||||
CREATE TABLE foo (
|
CREATE TABLE foo (
|
||||||
id INT NOT NULL PRIMARY KEY,
|
id INT NOT NULL PRIMARY KEY,
|
||||||
content VARCHAR
|
content VARCHAR
|
||||||
|
@ -133,20 +135,51 @@ CREATE TABLE foo (
|
||||||
|
|
||||||
all: SELECT foo.** FROM foo UNION ALL SELECT foo.** FROM foo;
|
all: SELECT foo.** FROM foo UNION ALL SELECT foo.** FROM foo;
|
||||||
''',
|
''',
|
||||||
});
|
});
|
||||||
|
|
||||||
final result = await state.analyze('package:foo/a.moor');
|
final result = await state.analyze('package:foo/a.moor');
|
||||||
state.close();
|
state.close();
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
result.errors.errors,
|
result.errors.errors,
|
||||||
contains(isA<MoorError>().having(
|
contains(isA<MoorError>().having(
|
||||||
(e) => e.message,
|
(e) => e.message,
|
||||||
'message',
|
'message',
|
||||||
contains('may only appear in a top-level select'),
|
contains('columns may only appear in a top-level select'),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
timeout: Timeout.none,
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'warns when nested query appear in nested query',
|
||||||
|
() async {
|
||||||
|
final state = TestState.withContent({
|
||||||
|
'foo|lib/a.moor': '''
|
||||||
|
CREATE TABLE foo (
|
||||||
|
id INT NOT NULL PRIMARY KEY,
|
||||||
|
content VARCHAR
|
||||||
|
);
|
||||||
|
|
||||||
|
all: SELECT foo.**, LIST(SELECT *, LIST(SELECT * FROM foo) FROM foo) FROM foo;
|
||||||
|
''',
|
||||||
|
});
|
||||||
|
|
||||||
|
final result = await state.analyze('package:foo/a.moor');
|
||||||
|
state.close();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
result.errors.errors,
|
||||||
|
contains(isA<MoorError>().having(
|
||||||
|
(e) => e.message,
|
||||||
|
'message',
|
||||||
|
contains('query may only appear in a top-level select'),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
timeout: Timeout.none,
|
||||||
|
);
|
||||||
|
|
||||||
group('warns about insert column count mismatch', () {
|
group('warns about insert column count mismatch', () {
|
||||||
TestState state;
|
TestState state;
|
||||||
|
|
|
@ -43,7 +43,7 @@ Future<void> main() async {
|
||||||
SqlQuery parse(String sql) {
|
SqlQuery parse(String sql) {
|
||||||
final parsed = engine.analyze(sql);
|
final parsed = engine.analyze(sql);
|
||||||
final fakeQuery = DeclaredDartQuery('query', sql);
|
final fakeQuery = DeclaredDartQuery('query', sql);
|
||||||
return QueryHandler(fakeQuery, parsed, mapper).handle();
|
return QueryHandler(parsed, mapper).handle(fakeQuery);
|
||||||
}
|
}
|
||||||
|
|
||||||
group('detects whether multiple tables are referenced', () {
|
group('detects whether multiple tables are referenced', () {
|
||||||
|
@ -105,9 +105,16 @@ FROM routes
|
||||||
|
|
||||||
expect(resultSet.columns.map((e) => e.name), ['id', 'from', 'to']);
|
expect(resultSet.columns.map((e) => e.name), ['id', 'from', 'to']);
|
||||||
expect(resultSet.matchingTable, isNull);
|
expect(resultSet.matchingTable, isNull);
|
||||||
expect(resultSet.nestedResults.map((e) => e.name), ['from', 'to']);
|
expect(
|
||||||
expect(resultSet.nestedResults.map((e) => e.table.displayName),
|
resultSet.nestedResults.cast<NestedResultTable>().map((e) => e.name),
|
||||||
['points', 'points']);
|
['from', 'to'],
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
resultSet.nestedResults
|
||||||
|
.cast<NestedResultTable>()
|
||||||
|
.map((e) => e.table.displayName),
|
||||||
|
['points', 'points'],
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('resolves nullability of aliases in nested result sets', () async {
|
test('resolves nullability of aliases in nested result sets', () async {
|
||||||
|
@ -144,9 +151,14 @@ LEFT JOIN tableB1 AS tableB2 -- nullable
|
||||||
final resultSet = (query as SqlSelectQuery).resultSet;
|
final resultSet = (query as SqlSelectQuery).resultSet;
|
||||||
|
|
||||||
final nested = resultSet.nestedResults;
|
final nested = resultSet.nestedResults;
|
||||||
expect(nested.map((e) => e.name),
|
expect(
|
||||||
['tableA1', 'tableA2', 'tableB1', 'tableB2']);
|
nested.cast<NestedResultTable>().map((e) => e.name),
|
||||||
expect(nested.map((e) => e.isNullable), [false, true, false, true]);
|
['tableA1', 'tableA2', 'tableB1', 'tableB2'],
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
nested.cast<NestedResultTable>().map((e) => e.isNullable),
|
||||||
|
[false, true, false, true],
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('infers result set for views', () async {
|
test('infers result set for views', () async {
|
||||||
|
|
|
@ -18,7 +18,9 @@ void main() {
|
||||||
final result = engine.analyze(
|
final result = engine.analyze(
|
||||||
'SELECT * FROM todos WHERE title = ?2 OR id IN ? OR title = ?1');
|
'SELECT * FROM todos WHERE title = ?2 OR id IN ? OR title = ?1');
|
||||||
|
|
||||||
final elements = mapper.extractElements(result).cast<FoundVariable>();
|
final elements = mapper
|
||||||
|
.extractElements(ctx: result, root: result.root)
|
||||||
|
.cast<FoundVariable>();
|
||||||
|
|
||||||
expect(elements.map((v) => v.index), [1, 2, 3]);
|
expect(elements.map((v) => v.index), [1, 2, 3]);
|
||||||
});
|
});
|
||||||
|
@ -26,14 +28,45 @@ void main() {
|
||||||
test('throws when an array with an explicit index is used', () {
|
test('throws when an array with an explicit index is used', () {
|
||||||
final result = engine.analyze('SELECT 1 WHERE 1 IN ?1');
|
final result = engine.analyze('SELECT 1 WHERE 1 IN ?1');
|
||||||
|
|
||||||
expect(() => mapper.extractElements(result), throwsArgumentError);
|
expect(() => mapper.extractElements(ctx: result, root: result.root),
|
||||||
|
throwsArgumentError);
|
||||||
});
|
});
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'throws when an explicitly index var with higher index appears after array',
|
'throws when an explicitly index var with higher index appears after array',
|
||||||
() {
|
() {
|
||||||
final result = engine.analyze('SELECT 1 WHERE 1 IN ? OR 2 = ?2');
|
final result = engine.analyze('SELECT 1 WHERE 1 IN ? OR 2 = ?2');
|
||||||
expect(() => mapper.extractElements(result), throwsArgumentError);
|
expect(() => mapper.extractElements(ctx: result, root: result.root),
|
||||||
|
throwsArgumentError);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
test('extracts variables but excludes nested queries', () {
|
||||||
|
final result = engine.analyze(
|
||||||
|
'SELECT *, LIST(SELECT * FROM todos WHERE title = ?3)'
|
||||||
|
'FROM todos WHERE title = ?2 OR id IN ? OR title = ?1',
|
||||||
|
);
|
||||||
|
|
||||||
|
final elements = mapper
|
||||||
|
.extractElements(ctx: result, root: result.root)
|
||||||
|
.cast<FoundVariable>();
|
||||||
|
|
||||||
|
expect(elements.map((v) => v.index), [1, 2, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('extracts variables from nested query', () {
|
||||||
|
final result = engine.analyze(
|
||||||
|
'SELECT *, LIST(SELECT * FROM todos WHERE title = ?1)'
|
||||||
|
'FROM todos WHERE title = ?2 OR id IN ? OR title = ?1',
|
||||||
|
);
|
||||||
|
|
||||||
|
final root =
|
||||||
|
((result.root as SelectStatement).columns[1] as NestedQueryColumn)
|
||||||
|
.select;
|
||||||
|
|
||||||
|
final elements =
|
||||||
|
mapper.extractElements(ctx: result, root: root).cast<FoundVariable>();
|
||||||
|
|
||||||
|
expect(elements.map((v) => v.index), [1]);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ void main() {
|
||||||
final writer = Writer(
|
final writer = Writer(
|
||||||
const MoorOptions.defaults(generateNamedParameters: true),
|
const MoorOptions.defaults(generateNamedParameters: true),
|
||||||
generationOptions: const GenerationOptions(nnbd: true));
|
generationOptions: const GenerationOptions(nnbd: true));
|
||||||
QueryWriter(fileState.resolvedQueries!.single, writer.child()).write();
|
QueryWriter(writer.child()).write(fileState.resolvedQueries!.single);
|
||||||
|
|
||||||
expect(writer.writeGenerated(), contains('required List<int?> idList'));
|
expect(writer.writeGenerated(), contains('required List<int?> idList'));
|
||||||
});
|
});
|
||||||
|
@ -47,7 +47,7 @@ void main() {
|
||||||
final writer = Writer(
|
final writer = Writer(
|
||||||
const MoorOptions.defaults(newSqlCodeGeneration: true),
|
const MoorOptions.defaults(newSqlCodeGeneration: true),
|
||||||
generationOptions: const GenerationOptions(nnbd: true));
|
generationOptions: const GenerationOptions(nnbd: true));
|
||||||
QueryWriter(fileState.resolvedQueries!.single, writer.child()).write();
|
QueryWriter(writer.child()).write(fileState.resolvedQueries!.single);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
writer.writeGenerated(),
|
writer.writeGenerated(),
|
||||||
|
@ -58,6 +58,35 @@ void main() {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('generates correct name for renamed nested star columns', () async {
|
||||||
|
final state = TestState.withContent({
|
||||||
|
'a|lib/main.moor': '''
|
||||||
|
CREATE TABLE tbl (
|
||||||
|
id INTEGER NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
query: SELECT t.** AS tableName FROM tbl AS t;
|
||||||
|
''',
|
||||||
|
}, enableAnalyzer: false);
|
||||||
|
addTearDown(state.close);
|
||||||
|
|
||||||
|
final file = await state.analyze('package:a/main.moor');
|
||||||
|
final fileState = file.currentResult as ParsedMoorFile;
|
||||||
|
|
||||||
|
final writer = Writer(
|
||||||
|
const MoorOptions.defaults(newSqlCodeGeneration: true),
|
||||||
|
generationOptions: const GenerationOptions(nnbd: true));
|
||||||
|
QueryWriter(writer.child()).write(fileState.resolvedQueries!.single);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
writer.writeGenerated(),
|
||||||
|
allOf(
|
||||||
|
contains('SELECT"t"."id" AS "nested_0.id"'),
|
||||||
|
contains('final TblData tableName;'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
group('generates correct code for expanded arrays', () {
|
group('generates correct code for expanded arrays', () {
|
||||||
late TestState state;
|
late TestState state;
|
||||||
|
|
||||||
|
@ -87,7 +116,7 @@ void main() {
|
||||||
options,
|
options,
|
||||||
generationOptions: const GenerationOptions(nnbd: true),
|
generationOptions: const GenerationOptions(nnbd: true),
|
||||||
);
|
);
|
||||||
QueryWriter(fileState.resolvedQueries!.single, writer.child()).write();
|
QueryWriter(writer.child()).write(fileState.resolvedQueries!.single);
|
||||||
|
|
||||||
expect(writer.writeGenerated(), expectation);
|
expect(writer.writeGenerated(), expectation);
|
||||||
}
|
}
|
||||||
|
@ -118,4 +147,76 @@ void main() {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('generates correct code for nested queries', () {
|
||||||
|
late TestState state;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
state = TestState.withContent({
|
||||||
|
'a|lib/main.moor': '''
|
||||||
|
CREATE TABLE tbl (
|
||||||
|
a TEXT,
|
||||||
|
b TEXT,
|
||||||
|
c TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
query: SELECT a, LIST(SELECT b, c FROM tbl WHERE a = :a AND b = :b) FROM tbl WHERE a = :a;
|
||||||
|
''',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() => state.close());
|
||||||
|
|
||||||
|
Future<void> _runTest(
|
||||||
|
MoorOptions options, List<Matcher> expectation) async {
|
||||||
|
final file = await state.analyze('package:a/main.moor');
|
||||||
|
final fileState = file.currentResult as ParsedMoorFile;
|
||||||
|
|
||||||
|
expect(file.errors.errors, isEmpty);
|
||||||
|
|
||||||
|
final writer = Writer(
|
||||||
|
options,
|
||||||
|
generationOptions: const GenerationOptions(nnbd: true),
|
||||||
|
);
|
||||||
|
QueryWriter(writer.child()).write(fileState.resolvedQueries!.single);
|
||||||
|
|
||||||
|
final result = writer.writeGenerated();
|
||||||
|
for (final e in expectation) {
|
||||||
|
expect(result, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test('should error with old generator', () async {
|
||||||
|
final file = await state.analyze('package:a/main.moor');
|
||||||
|
final fileState = file.currentResult as ParsedMoorFile;
|
||||||
|
|
||||||
|
expect(file.errors.errors, isEmpty);
|
||||||
|
|
||||||
|
final writer = Writer(
|
||||||
|
const MoorOptions.defaults(newSqlCodeGeneration: false),
|
||||||
|
generationOptions: const GenerationOptions(nnbd: true),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() => QueryWriter(writer.child())
|
||||||
|
.write(fileState.resolvedQueries!.single),
|
||||||
|
throwsA(isA<UnsupportedError>()),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('with the new query generator', () {
|
||||||
|
return _runTest(
|
||||||
|
const MoorOptions.defaults(newSqlCodeGeneration: true),
|
||||||
|
[
|
||||||
|
contains('SELECT a FROM tbl WHERE a = ?1'),
|
||||||
|
contains('SELECT b, c FROM tbl WHERE a = ?1 AND b = ?2'),
|
||||||
|
contains('nestedQuery0: await'),
|
||||||
|
contains('variables: [Variable<String?>(a), Variable<String?>(b)]'),
|
||||||
|
contains('b: row.read<String?>(\'b\')'),
|
||||||
|
contains('c: row.read<String?>(\'c\')'),
|
||||||
|
contains('class QueryNestedQuery0'),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,8 +9,8 @@ void main() {
|
||||||
void check(String sql, String expectedDart) {
|
void check(String sql, String expectedDart) {
|
||||||
final engine = SqlEngine();
|
final engine = SqlEngine();
|
||||||
final context = engine.analyze(sql);
|
final context = engine.analyze(sql);
|
||||||
final query = SqlSelectQuery(
|
final query = SqlSelectQuery('name', context, context.root, [], [],
|
||||||
'name', context, [], [], InferredResultSet(null, []), null);
|
InferredResultSet(null, []), null, null);
|
||||||
|
|
||||||
final result =
|
final result =
|
||||||
SqlWriter(const MoorOptions.defaults(), query: query).write();
|
SqlWriter(const MoorOptions.defaults(), query: query).write();
|
||||||
|
|
|
@ -12,6 +12,7 @@ export 'types/types.dart' show TypeInferenceResults;
|
||||||
|
|
||||||
part 'context.dart';
|
part 'context.dart';
|
||||||
part 'error.dart';
|
part 'error.dart';
|
||||||
|
part 'options.dart';
|
||||||
part 'schema/column.dart';
|
part 'schema/column.dart';
|
||||||
part 'schema/from_create_table.dart';
|
part 'schema/from_create_table.dart';
|
||||||
part 'schema/references.dart';
|
part 'schema/references.dart';
|
||||||
|
@ -23,7 +24,6 @@ part 'steps/linting_visitor.dart';
|
||||||
part 'steps/prepare_ast.dart';
|
part 'steps/prepare_ast.dart';
|
||||||
part 'steps/reference_resolver.dart';
|
part 'steps/reference_resolver.dart';
|
||||||
part 'steps/set_parent_visitor.dart';
|
part 'steps/set_parent_visitor.dart';
|
||||||
part 'options.dart';
|
|
||||||
part 'utils/expand_function_parameters.dart';
|
part 'utils/expand_function_parameters.dart';
|
||||||
|
|
||||||
/// Something that can be represented in a human-readable description.
|
/// Something that can be represented in a human-readable description.
|
||||||
|
|
|
@ -320,6 +320,8 @@ class ColumnResolver extends RecursiveVisitor<void, void> {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (target != null) resultColumn.resultSet = target.resultSet.resultSet;
|
if (target != null) resultColumn.resultSet = target.resultSet.resultSet;
|
||||||
|
} else if (resultColumn is NestedQueryColumn) {
|
||||||
|
_resolveSelect(resultColumn.select);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ class AstPreparingVisitor extends RecursiveVisitor<void, void> {
|
||||||
|
|
||||||
void start(AstNode root) {
|
void start(AstNode root) {
|
||||||
root.accept(this, null);
|
root.accept(this, null);
|
||||||
_resolveIndexOfVariables();
|
resolveIndexOfVariables(_foundVariables);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -170,16 +170,16 @@ class AstPreparingVisitor extends RecursiveVisitor<void, void> {
|
||||||
visitChildren(e, arg);
|
visitChildren(e, arg);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _resolveIndexOfVariables() {
|
static void resolveIndexOfVariables(List<Variable> variables) {
|
||||||
// sort variables by the order in which they appear inside the statement.
|
// sort variables by the order in which they appear inside the statement.
|
||||||
_foundVariables.sort((a, b) {
|
variables.sort((a, b) {
|
||||||
return a.firstPosition.compareTo(b.firstPosition);
|
return a.firstPosition.compareTo(b.firstPosition);
|
||||||
});
|
});
|
||||||
// Assigning rules are explained at https://www.sqlite.org/lang_expr.html#varparam
|
// Assigning rules are explained at https://www.sqlite.org/lang_expr.html#varparam
|
||||||
var largestAssigned = 0;
|
var largestAssigned = 0;
|
||||||
final resolvedNames = <String, int>{};
|
final resolvedNames = <String, int>{};
|
||||||
|
|
||||||
for (final variable in _foundVariables) {
|
for (final variable in variables) {
|
||||||
if (variable is NumberedVariable) {
|
if (variable is NumberedVariable) {
|
||||||
// if the variable has an explicit index (e.g ?123), then 123 is the
|
// if the variable has an explicit index (e.g ?123), then 123 is the
|
||||||
// resolved index and the next variable will have index 124. Otherwise,
|
// resolved index and the next variable will have index 124. Otherwise,
|
||||||
|
@ -249,4 +249,18 @@ class AstPreparingVisitor extends RecursiveVisitor<void, void> {
|
||||||
|
|
||||||
visitChildren(e, null);
|
visitChildren(e, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// If a nested query was found. Collect everything separately.
|
||||||
|
@override
|
||||||
|
void visitMoorSpecificNode(MoorSpecificNode e, void arg) {
|
||||||
|
if (e is NestedQueryColumn) {
|
||||||
|
// create a new scope for the nested query to differentiate between
|
||||||
|
// references that can be resolved in the nested query and references
|
||||||
|
// which require data from the parent query
|
||||||
|
e.select.scope = e.scope.createChild();
|
||||||
|
AstPreparingVisitor(context: context).start(e.select);
|
||||||
|
} else {
|
||||||
|
super.visitMoorSpecificNode(e, arg);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -138,6 +138,7 @@ class ReferenceResolver extends RecursiveVisitor<void, void> {
|
||||||
column = AvailableColumn(column, source);
|
column = AvailableColumn(column, source);
|
||||||
}
|
}
|
||||||
ref.resolved = column;
|
ref.resolved = column;
|
||||||
|
ref.resultEntity = source;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -47,6 +47,8 @@ class TypeResolver extends RecursiveVisitor<TypeExpectation, void> {
|
||||||
currentColumnIndex++;
|
currentColumnIndex++;
|
||||||
} else if (child is StarResultColumn) {
|
} else if (child is StarResultColumn) {
|
||||||
currentColumnIndex += child.scope.availableColumns.length;
|
currentColumnIndex += child.scope.availableColumns.length;
|
||||||
|
} else if (child is NestedQueryColumn) {
|
||||||
|
visit(child.select, arg);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
visit(child, arg);
|
visit(child, arg);
|
||||||
|
|
|
@ -18,6 +18,7 @@ export 'moor/declared_statement.dart';
|
||||||
export 'moor/import_statement.dart';
|
export 'moor/import_statement.dart';
|
||||||
export 'moor/inline_dart.dart';
|
export 'moor/inline_dart.dart';
|
||||||
export 'moor/moor_file.dart';
|
export 'moor/moor_file.dart';
|
||||||
|
export 'moor/nested_query_column.dart';
|
||||||
export 'moor/nested_star_result_column.dart';
|
export 'moor/nested_star_result_column.dart';
|
||||||
export 'node.dart';
|
export 'node.dart';
|
||||||
export 'statements/block.dart';
|
export 'statements/block.dart';
|
||||||
|
|
|
@ -29,13 +29,14 @@ class NumberedVariable extends Expression implements Variable {
|
||||||
}
|
}
|
||||||
|
|
||||||
class ColonNamedVariable extends Expression implements Variable {
|
class ColonNamedVariable extends Expression implements Variable {
|
||||||
final ColonVariableToken token;
|
final String name;
|
||||||
String get name => token.name;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int? resolvedIndex;
|
int? resolvedIndex;
|
||||||
|
|
||||||
ColonNamedVariable(this.token);
|
ColonNamedVariable.synthetic(this.name);
|
||||||
|
|
||||||
|
ColonNamedVariable(ColonVariableToken token) : name = token.name;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
R accept<A, R>(AstVisitor<A, R> visitor, A arg) {
|
R accept<A, R>(AstVisitor<A, R> visitor, A arg) {
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
import '../ast.dart' show ResultColumn, Renamable, SelectStatement;
|
||||||
|
import '../node.dart';
|
||||||
|
import '../visitor.dart';
|
||||||
|
import 'moor_file.dart';
|
||||||
|
|
||||||
|
/// To wrap the query name into its own type, to avoid conflicts when using
|
||||||
|
/// the [AstNode] metadata.
|
||||||
|
class _NestedColumnNameMetadata {
|
||||||
|
final String? name;
|
||||||
|
|
||||||
|
_NestedColumnNameMetadata(this.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A nested query column, denoted by `LIST(...)` in user queries.
|
||||||
|
///
|
||||||
|
/// Nested query columns take a select query and execute it for every result
|
||||||
|
/// returned from the main query. Nested query columns can only be added to a
|
||||||
|
/// top level select query, because the result of them can only be computed
|
||||||
|
/// in dart.
|
||||||
|
class NestedQueryColumn extends ResultColumn
|
||||||
|
implements MoorSpecificNode, Renamable {
|
||||||
|
@override
|
||||||
|
final String? as;
|
||||||
|
|
||||||
|
SelectStatement select;
|
||||||
|
|
||||||
|
NestedQueryColumn({required this.select, this.as});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<AstNode> get childNodes => [select];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void transformChildren<A>(Transformer<A> transformer, A arg) {
|
||||||
|
select = transformer.transformChild(select, this, arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
R accept<A, R>(AstVisitor<A, R> visitor, A arg) {
|
||||||
|
return visitor.visitMoorSpecificNode(this, arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The unique name for this query. Used to identify it and it's variables in
|
||||||
|
/// the AST tree.
|
||||||
|
set queryName(String? name) => setMeta(_NestedColumnNameMetadata(name));
|
||||||
|
|
||||||
|
String? get queryName => meta<_NestedColumnNameMetadata>()?.name;
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import '../../analysis/analysis.dart';
|
import '../../analysis/analysis.dart';
|
||||||
import '../ast.dart' show StarResultColumn, ResultColumn;
|
import '../ast.dart' show StarResultColumn, ResultColumn, Renamable;
|
||||||
import '../node.dart';
|
import '../node.dart';
|
||||||
import '../visitor.dart';
|
import '../visitor.dart';
|
||||||
import 'moor_file.dart';
|
import 'moor_file.dart';
|
||||||
|
@ -9,11 +9,15 @@ import 'moor_file.dart';
|
||||||
/// Nested star result columns behave similar to a regular [StarResultColumn]
|
/// Nested star result columns behave similar to a regular [StarResultColumn]
|
||||||
/// when the query is actually run. However, they will affect generated code
|
/// when the query is actually run. However, they will affect generated code
|
||||||
/// when using moor.
|
/// when using moor.
|
||||||
class NestedStarResultColumn extends ResultColumn implements MoorSpecificNode {
|
class NestedStarResultColumn extends ResultColumn
|
||||||
|
implements MoorSpecificNode, Renamable {
|
||||||
final String tableName;
|
final String tableName;
|
||||||
ResultSet? resultSet;
|
ResultSet? resultSet;
|
||||||
|
|
||||||
NestedStarResultColumn(this.tableName);
|
@override
|
||||||
|
final String? as;
|
||||||
|
|
||||||
|
NestedStarResultColumn({required this.tableName, this.as});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Iterable<AstNode> get childNodes => const [];
|
Iterable<AstNode> get childNodes => const [];
|
||||||
|
|
|
@ -78,6 +78,8 @@ abstract class AstNode with HasMetaMixin implements SyntacticEntity {
|
||||||
yield* allDescendants;
|
yield* allDescendants;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isChildOf(AstNode other) => parents.contains(other);
|
||||||
|
|
||||||
/// Finds the first element in [selfAndParents] of the type [T].
|
/// Finds the first element in [selfAndParents] of the type [T].
|
||||||
///
|
///
|
||||||
/// Returns `null` if there's no node of type [T] surrounding this ast node.
|
/// Returns `null` if there's no node of type [T] surrounding this ast node.
|
||||||
|
|
|
@ -1918,8 +1918,12 @@ class Parser {
|
||||||
return StarResultColumn(identifier.identifier)
|
return StarResultColumn(identifier.identifier)
|
||||||
..setSpan(identifier, _previous);
|
..setSpan(identifier, _previous);
|
||||||
} else if (enableMoorExtensions && _matchOne(TokenType.doubleStar)) {
|
} else if (enableMoorExtensions && _matchOne(TokenType.doubleStar)) {
|
||||||
return NestedStarResultColumn(identifier.identifier)
|
final as = _as();
|
||||||
..setSpan(identifier, _previous);
|
|
||||||
|
return NestedStarResultColumn(
|
||||||
|
tableName: identifier.identifier,
|
||||||
|
as: as?.identifier,
|
||||||
|
)..setSpan(identifier, _previous);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1929,6 +1933,30 @@ class Parser {
|
||||||
_current = positionBefore;
|
_current = positionBefore;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parsing for the nested query column
|
||||||
|
if (enableMoorExtensions && _matchOne(TokenType.list)) {
|
||||||
|
final list = _previous;
|
||||||
|
|
||||||
|
_consume(
|
||||||
|
TokenType.leftParen,
|
||||||
|
'Expected opening parenthesis after LIST',
|
||||||
|
);
|
||||||
|
|
||||||
|
final statement = _fullSelect();
|
||||||
|
if (statement == null || statement is! SelectStatement) {
|
||||||
|
_error('Expected a select statement here');
|
||||||
|
}
|
||||||
|
|
||||||
|
_consume(
|
||||||
|
TokenType.rightParen,
|
||||||
|
'Expected closing parenthesis to finish LIST expression',
|
||||||
|
);
|
||||||
|
|
||||||
|
final as = _as();
|
||||||
|
return NestedQueryColumn(select: statement, as: as?.identifier)
|
||||||
|
..setSpan(list, _previous);
|
||||||
|
}
|
||||||
|
|
||||||
final tokenBefore = _peek;
|
final tokenBefore = _peek;
|
||||||
|
|
||||||
final expr = expression();
|
final expr = expression();
|
||||||
|
|
|
@ -218,6 +218,7 @@ enum TokenType {
|
||||||
import,
|
import,
|
||||||
json,
|
json,
|
||||||
required,
|
required,
|
||||||
|
list,
|
||||||
|
|
||||||
/// A `**` token. This is only scanned when scanning for moor tokens.
|
/// A `**` token. This is only scanned when scanning for moor tokens.
|
||||||
doubleStar,
|
doubleStar,
|
||||||
|
@ -413,6 +414,7 @@ const Map<String, TokenType> moorKeywords = {
|
||||||
'JSON': TokenType.json,
|
'JSON': TokenType.json,
|
||||||
'MAPPED': TokenType.mapped,
|
'MAPPED': TokenType.mapped,
|
||||||
'REQUIRED': TokenType.required,
|
'REQUIRED': TokenType.required,
|
||||||
|
'LIST': TokenType.list,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A set of [TokenType]s that can be parsed as an identifier.
|
/// A set of [TokenType]s that can be parsed as an identifier.
|
||||||
|
|
|
@ -439,6 +439,13 @@ class EqualityEnforcingVisitor implements AstVisitor<void, void> {
|
||||||
void visitMoorNestedStarResultColumn(NestedStarResultColumn e, void arg) {
|
void visitMoorNestedStarResultColumn(NestedStarResultColumn e, void arg) {
|
||||||
final current = _currentAs<NestedStarResultColumn>(e);
|
final current = _currentAs<NestedStarResultColumn>(e);
|
||||||
_assert(current.tableName == e.tableName, e);
|
_assert(current.tableName == e.tableName, e);
|
||||||
|
_assert(current.as == e.as, e);
|
||||||
|
_checkChildren(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
void visitMoorNestedQueryColumn(NestedQueryColumn e, void arg) {
|
||||||
|
final current = _currentAs<NestedQueryColumn>(e);
|
||||||
|
_assert(current.as == e.as, e);
|
||||||
_checkChildren(e);
|
_checkChildren(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -458,6 +465,8 @@ class EqualityEnforcingVisitor implements AstVisitor<void, void> {
|
||||||
return visitMoorStatementParameter(e, arg);
|
return visitMoorStatementParameter(e, arg);
|
||||||
} else if (e is MoorTableName) {
|
} else if (e is MoorTableName) {
|
||||||
return visitMoorTableName(e, arg);
|
return visitMoorTableName(e, arg);
|
||||||
|
} else if (e is NestedQueryColumn) {
|
||||||
|
return visitMoorNestedQueryColumn(e, arg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -437,9 +437,6 @@ class NodeSqlBuilder extends AstVisitor<void, void> {
|
||||||
_keyword(TokenType.import);
|
_keyword(TokenType.import);
|
||||||
_stringLiteral(e.importedFile);
|
_stringLiteral(e.importedFile);
|
||||||
symbol(';', spaceAfter: true);
|
symbol(';', spaceAfter: true);
|
||||||
} else if (e is NestedStarResultColumn) {
|
|
||||||
identifier(e.tableName);
|
|
||||||
symbol('.**', spaceAfter: true);
|
|
||||||
} else if (e is StatementParameter) {
|
} else if (e is StatementParameter) {
|
||||||
if (e is VariableTypeHint) {
|
if (e is VariableTypeHint) {
|
||||||
if (e.isRequired) _keyword(TokenType.required);
|
if (e.isRequired) _keyword(TokenType.required);
|
||||||
|
@ -468,6 +465,10 @@ class NodeSqlBuilder extends AstVisitor<void, void> {
|
||||||
} else if (e is NestedStarResultColumn) {
|
} else if (e is NestedStarResultColumn) {
|
||||||
identifier(e.tableName);
|
identifier(e.tableName);
|
||||||
symbol('.**', spaceAfter: true);
|
symbol('.**', spaceAfter: true);
|
||||||
|
} else if (e is NestedQueryColumn) {
|
||||||
|
symbol('LIST(');
|
||||||
|
visit(e.select, arg);
|
||||||
|
symbol(')', spaceAfter: true);
|
||||||
} else if (e is TransactionBlock) {
|
} else if (e is TransactionBlock) {
|
||||||
visit(e.begin, arg);
|
visit(e.begin, arg);
|
||||||
_writeStatements(e.innerStatements);
|
_writeStatements(e.innerStatements);
|
||||||
|
@ -1258,7 +1259,7 @@ class NodeSqlBuilder extends AstVisitor<void, void> {
|
||||||
/// Writes an identifier, escaping it if necessary.
|
/// Writes an identifier, escaping it if necessary.
|
||||||
void identifier(String identifier,
|
void identifier(String identifier,
|
||||||
{bool spaceBefore = true, bool spaceAfter = true}) {
|
{bool spaceBefore = true, bool spaceAfter = true}) {
|
||||||
if (isKeywordLexeme(identifier) || identifier.contains(' ')) {
|
if (isKeywordLexeme(identifier) || _notAKeywordRegex.hasMatch(identifier)) {
|
||||||
identifier = '"$identifier"';
|
identifier = '"$identifier"';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1343,3 +1344,6 @@ extension NodeToText on AstNode {
|
||||||
return builder.buffer.toString();
|
return builder.buffer.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Allow 0, 1, 2, 3 in https://github.com/sqlite/sqlite/blob/665b6b6b35f10a46bb72377ade7c2e5d8ea42cb3/src/tokenize.c#L62-L80
|
||||||
|
final _notAKeywordRegex = RegExp('[^A-Za-z_0-9]');
|
||||||
|
|
|
@ -112,6 +112,25 @@ void main() {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('resolves columns in nested queries', () {
|
||||||
|
final engine = SqlEngine(EngineOptions(useMoorExtensions: true))
|
||||||
|
..registerTable(demoTable);
|
||||||
|
|
||||||
|
final context =
|
||||||
|
engine.analyze('SELECT content, LIST(SELECT id FROM demo) FROM demo');
|
||||||
|
|
||||||
|
expect(context.errors, isEmpty);
|
||||||
|
|
||||||
|
final select = context.root as SelectStatement;
|
||||||
|
final nestedQuery = select.columns[1] as NestedQueryColumn;
|
||||||
|
|
||||||
|
expect(nestedQuery.select.columns, hasLength(1));
|
||||||
|
expect(
|
||||||
|
context.typeOf(nestedQuery.select.resolvedColumns!.single).type!.type,
|
||||||
|
BasicType.int,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
group('reports correct column name for rowid aliases', () {
|
group('reports correct column name for rowid aliases', () {
|
||||||
final engine = SqlEngine()
|
final engine = SqlEngine()
|
||||||
..registerTable(demoTable)
|
..registerTable(demoTable)
|
||||||
|
|
|
@ -17,7 +17,7 @@ all: SELECT /* COUNT(*), */ * FROM tbl WHERE $predicate;
|
||||||
@special: SELECT * FROM tbl;
|
@special: SELECT * FROM tbl;
|
||||||
typeHints(REQUIRED :foo AS TEXT OR NULL, $predicate = TRUE):
|
typeHints(REQUIRED :foo AS TEXT OR NULL, $predicate = TRUE):
|
||||||
SELECT :foo WHERE $predicate;
|
SELECT :foo WHERE $predicate;
|
||||||
nested AS MyResultSet: SELECT foo.** FROM tbl foo;
|
nested AS MyResultSet: SELECT foo.** AS fooRename FROM tbl foo;
|
||||||
add: INSERT INTO tbl $row RETURNING *;
|
add: INSERT INTO tbl $row RETURNING *;
|
||||||
''';
|
''';
|
||||||
|
|
||||||
|
@ -103,7 +103,12 @@ void main() {
|
||||||
DeclaredStatement(
|
DeclaredStatement(
|
||||||
SimpleName('nested'),
|
SimpleName('nested'),
|
||||||
SelectStatement(
|
SelectStatement(
|
||||||
columns: [NestedStarResultColumn('foo')],
|
columns: [
|
||||||
|
NestedStarResultColumn(
|
||||||
|
tableName: 'foo',
|
||||||
|
as: 'fooRename',
|
||||||
|
)
|
||||||
|
],
|
||||||
from: TableReference('tbl', as: 'foo'),
|
from: TableReference('tbl', as: 'foo'),
|
||||||
),
|
),
|
||||||
as: 'MyResultSet',
|
as: 'MyResultSet',
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
import 'package:sqlparser/sqlparser.dart';
|
||||||
|
import 'package:sqlparser/src/utils/ast_equality.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
import '../utils.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('parses nested query statements', () {
|
||||||
|
final stmt = SqlEngine(EngineOptions(useMoorExtensions: true))
|
||||||
|
.parse('SELECT LIST(SELECT * FROM test) FROM test')
|
||||||
|
.rootNode as SelectStatement;
|
||||||
|
|
||||||
|
enforceHasSpan(stmt);
|
||||||
|
return enforceEqual(
|
||||||
|
stmt.columns[0],
|
||||||
|
NestedQueryColumn(
|
||||||
|
select: SelectStatement(
|
||||||
|
columns: [StarResultColumn(null)],
|
||||||
|
from: TableReference('test'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('parses nested query statements with as', () {
|
||||||
|
final stmt = SqlEngine(EngineOptions(useMoorExtensions: true))
|
||||||
|
.parse('SELECT LIST(SELECT * FROM test) AS newname FROM test')
|
||||||
|
.rootNode as SelectStatement;
|
||||||
|
|
||||||
|
enforceHasSpan(stmt);
|
||||||
|
return enforceEqual(
|
||||||
|
stmt.columns[0],
|
||||||
|
NestedQueryColumn(
|
||||||
|
as: 'newname',
|
||||||
|
select: SelectStatement(
|
||||||
|
columns: [StarResultColumn(null)],
|
||||||
|
from: TableReference('test'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
|
@ -423,6 +423,20 @@ CREATE UNIQUE INDEX my_idx ON t1 (c1, c2, c3) WHERE c1 < c3;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('identifiers', () {
|
||||||
|
void testWith(String id, String formatted) {
|
||||||
|
final node = Reference(columnName: id);
|
||||||
|
expect(node.toSql(), formatted);
|
||||||
|
}
|
||||||
|
|
||||||
|
testWith('a', 'a');
|
||||||
|
testWith('_', '_');
|
||||||
|
testWith('c0', 'c0');
|
||||||
|
testWith('_c0', '_c0');
|
||||||
|
testWith('a b', '"a b"');
|
||||||
|
testWith(r'a$b', r'"a$b"');
|
||||||
|
});
|
||||||
|
|
||||||
group('moor', () {
|
group('moor', () {
|
||||||
test('dart placeholders', () {
|
test('dart placeholders', () {
|
||||||
testFormat(r'SELECT $placeholder FROM foo');
|
testFormat(r'SELECT $placeholder FROM foo');
|
||||||
|
|
Loading…
Reference in New Issue