New analyzer: Basic query support

This commit is contained in:
Simon Binder 2022-09-12 22:56:33 +02:00
parent e08ccdbcda
commit 2bd7596de6
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
13 changed files with 267 additions and 36 deletions

View File

@ -0,0 +1,68 @@
import 'package:sqlparser/sqlparser.dart';
/// Static analysis support for SQL functions available when using a
/// `NativeDatabase` or a `WasmDatabase` in drift.
class DriftNativeExtension implements Extension {
const DriftNativeExtension();
@override
void register(SqlEngine engine) {
engine.registerFunctionHandler(const _MoorFfiFunctions());
}
}
class _MoorFfiFunctions with ArgumentCountLinter implements FunctionHandler {
const _MoorFfiFunctions();
static const Set<String> _unaryFunctions = {
'sqrt',
'sin',
'cos',
'tan',
'asin',
'acos',
'atan'
};
@override
Set<String> get functionNames {
return const {'pow', 'current_time_millis', ..._unaryFunctions};
}
@override
int? argumentCountFor(String function) {
if (_unaryFunctions.contains(function)) {
return 1;
} else if (function == 'pow') {
return 2;
} else if (function == 'current_time_millis') {
return 0;
} else {
return null;
}
}
@override
ResolveResult inferArgumentType(
AnalysisContext context, SqlInvocation call, Expression argument) {
return const ResolveResult(
ResolvedType(type: BasicType.real, nullable: false));
}
@override
ResolveResult inferReturnType(AnalysisContext context, SqlInvocation call,
List<Typeable> expandedArgs) {
if (call.name == 'current_time_millis') {
return const ResolveResult(
ResolvedType(type: BasicType.int, nullable: false));
}
return const ResolveResult(
ResolvedType(type: BasicType.real, nullable: true));
}
@override
void reportErrors(SqlInvocation call, AnalysisContext context) {
reportMismatches(call, context);
}
}

View File

@ -2,6 +2,7 @@ import 'package:sqlparser/sqlparser.dart';
import '../../analyzer/options.dart';
import '../backend.dart';
import '../drift_native_functions.dart';
import '../resolver/dart/helper.dart';
import '../resolver/discover.dart';
import '../resolver/drift/sqlparser/mapping.dart';
@ -26,7 +27,12 @@ class DriftAnalysisDriver {
EngineOptions(
useDriftExtensions: true,
enabledExtensions: [
// todo: Map from options
if (options.hasModule(SqlModule.fts5)) const Fts5Extension(),
if (options.hasModule(SqlModule.json1)) const Json1Extension(),
if (options.hasModule(SqlModule.moor_ffi))
const DriftNativeExtension(),
if (options.hasModule(SqlModule.math)) const BuiltInMathExtension(),
if (options.hasModule(SqlModule.rtree)) const RTreeExtension(),
],
version: options.sqliteVersion,
),

View File

@ -20,9 +20,36 @@ class DiscoverStep {
DriftElementId _id(String name) => DriftElementId(_file.ownUri, name);
List<DiscoveredElement> _checkForDuplicates(List<DiscoveredElement> source) {
final ids = <DriftElementId>{};
final result = <DiscoveredElement>[];
for (final found in source) {
if (ids.add(found.ownId)) {
result.add(found);
} else {
final DriftAnalysisError error;
final msg =
'This file already defines an element named `${found.ownId.name}`';
if (found is DiscoveredDriftElement) {
error = DriftAnalysisError.inDriftFile(found.sqlNode, msg);
} else if (found is DiscoveredDartElement) {
error = DriftAnalysisError.forDartElement(found.dartElement, msg);
} else {
error = DriftAnalysisError(null, msg);
}
_file.errorsDuringDiscovery.add(error);
}
}
return result;
}
Future<void> discover() async {
final extension = _file.extension;
final pendingElements = <DiscoveredElement>[];
switch (extension) {
case '.dart':
@ -33,7 +60,8 @@ class DiscoverStep {
await finder.find();
_file.errorsDuringDiscovery.addAll(finder.errors);
_file.discovery = DiscoveredDartLibrary(library, finder.found);
_file.discovery =
DiscoveredDartLibrary(library, _checkForDuplicates(finder.found));
} catch (e, s) {
_driver.backend.log
.fine('Could not read Dart library from ${_file.ownUri}', e, s);
@ -42,6 +70,8 @@ class DiscoverStep {
break;
case '.drift':
final engine = _driver.newSqlEngine();
final pendingElements = <DiscoveredDriftElement>[];
String contents;
try {
contents = await _driver.backend.readAsString(_file.ownUri);
@ -61,6 +91,8 @@ class DiscoverStep {
final ast = parsed.rootNode as DriftFile;
final imports = <DriftFileImport>[];
var specialQueryNameCount = 0;
for (final node in ast.childNodes) {
if (node is ImportStatement) {
final uri =
@ -79,6 +111,17 @@ class DiscoverStep {
} else if (node is CreateTriggerStatement) {
pendingElements
.add(DiscoveredDriftTrigger(_id(node.triggerName), node));
} else if (node is DeclaredStatement) {
String name;
final declaredName = node.identifier;
if (declaredName is SimpleName) {
name = declaredName.name;
} else {
name = 'special:${specialQueryNameCount++}';
}
pendingElements.add(DiscoveredDriftStatement(_id(name), node));
}
}
@ -86,7 +129,7 @@ class DiscoverStep {
originalSource: contents,
ast: parsed.rootNode as DriftFile,
imports: imports,
locallyDefinedElements: pendingElements,
locallyDefinedElements: _checkForDuplicates(pendingElements),
);
break;
}

View File

@ -10,7 +10,7 @@ class DriftIndexResolver extends DriftElementResolver<DiscoveredDriftIndex> {
@override
Future<DriftIndex> resolve() async {
final stmt = discovered.createIndex;
final stmt = discovered.sqlNode;
final references = await resolveSqlReferences(stmt);
final engine = newEngineWithTables(references);

View File

@ -0,0 +1,29 @@
import '../../driver/state.dart';
import '../../results/results.dart';
import '../intermediate_state.dart';
import 'element_resolver.dart';
class DriftQueryResolver
extends DriftElementResolver<DiscoveredDriftStatement> {
DriftQueryResolver(super.file, super.discovered, super.resolver, super.state);
@override
Future<DefinedSqlQuery> resolve() async {
final stmt = discovered.sqlNode.statement;
final references = await resolveSqlReferences(stmt);
final engine = newEngineWithTables(references);
final source = (file.discovery as DiscoveredDriftFile).originalSource;
final context = engine.analyzeNode(stmt, source);
reportLints(context);
return DefinedSqlQuery(
discovered.ownId,
DriftDeclaration.driftFile(stmt, file.ownUri),
references: references,
sql: source.substring(stmt.firstPosition, stmt.lastPosition),
sqlOffset: stmt.firstPosition,
);
}
}

View File

@ -17,6 +17,7 @@ class DriftTableResolver extends LocalElementResolver<DiscoveredDriftTable> {
Future<DriftTable> resolve() async {
Table table;
final references = <DriftElement>{};
final stmt = discovered.sqlNode;
try {
final reader = SchemaFromCreateTable(
@ -24,12 +25,12 @@ class DriftTableResolver extends LocalElementResolver<DiscoveredDriftTable> {
driftUseTextForDateTime:
resolver.driver.options.storeDateTimeValuesAsText,
);
table = reader.read(discovered.createTable);
table = reader.read(stmt);
} catch (e, s) {
resolver.driver.backend.log
.warning('Error reading table from internal statement', e, s);
reportError(DriftAnalysisError.inDriftFile(
discovered.createTable.tableNameToken ?? discovered.createTable,
stmt.tableNameToken ?? stmt,
'The structure of this table could not be extracted, possibly due to a '
'bug in drift_dev.',
));
@ -97,7 +98,7 @@ class DriftTableResolver extends LocalElementResolver<DiscoveredDriftTable> {
String? dartTableName, dataClassName;
ExistingRowClass? existingRowClass;
final driftTableInfo = discovered.createTable.driftTableName;
final driftTableInfo = stmt.driftTableName;
if (driftTableInfo != null) {
final overriddenNames = driftTableInfo.overriddenDataClassName;
@ -106,7 +107,7 @@ class DriftTableResolver extends LocalElementResolver<DiscoveredDriftTable> {
final clazz = await findDartClass(imports, overriddenNames);
if (clazz == null) {
reportError(DriftAnalysisError.inDriftFile(
discovered.createTable.tableNameToken!,
stmt.tableNameToken!,
'Existing Dart class $overriddenNames was not found, are '
'you missing an import?',
));
@ -133,7 +134,7 @@ class DriftTableResolver extends LocalElementResolver<DiscoveredDriftTable> {
discovered.ownId,
DriftDeclaration(
state.ownId.libraryUri,
discovered.createTable.firstPosition,
stmt.firstPosition,
),
columns: columns,
references: references.toList(),

View File

@ -14,7 +14,7 @@ class DriftTriggerResolver
@override
Future<DriftTrigger> resolve() async {
final stmt = discovered.createTrigger;
final stmt = discovered.sqlNode;
final references = await resolveSqlReferences(stmt);
final engine = newEngineWithTables(references);

View File

@ -15,7 +15,7 @@ class DriftViewResolver extends DriftElementResolver<DiscoveredDriftView> {
@override
Future<DriftView> resolve() async {
final stmt = discovered.createView;
final stmt = discovered.sqlNode;
final references = await resolveSqlReferences(stmt);
final engine = newEngineWithTables(references);
@ -76,14 +76,12 @@ class DriftViewResolver extends DriftElementResolver<DiscoveredDriftView> {
}
}
final discovery = file.discovery as DiscoveredDriftFile;
return DriftView(
discovered.ownId,
DriftDeclaration.driftFile(stmt, file.ownUri),
columns: columns,
source: SqlViewSource(discovery.originalSource
.substring(stmt.firstPosition, stmt.lastPosition)),
source: SqlViewSource(
source.substring(stmt.firstPosition, stmt.lastPosition)),
customParentClass: null,
entityInfoName: entityInfoName,
existingRowClass: existingRowClass,

View File

@ -3,29 +3,17 @@ import 'package:sqlparser/sqlparser.dart';
import '../driver/state.dart';
class DiscoveredDriftTable extends DiscoveredElement {
final TableInducingStatement createTable;
class DiscoveredDriftElement<AST extends AstNode> extends DiscoveredElement {
final AST sqlNode;
DiscoveredDriftTable(super.ownId, this.createTable);
DiscoveredDriftElement(super.ownId, this.sqlNode);
}
class DiscoveredDriftView extends DiscoveredElement {
final CreateViewStatement createView;
DiscoveredDriftView(super.ownId, this.createView);
}
class DiscoveredDriftIndex extends DiscoveredElement {
final CreateIndexStatement createIndex;
DiscoveredDriftIndex(super.ownId, this.createIndex);
}
class DiscoveredDriftTrigger extends DiscoveredElement {
final CreateTriggerStatement createTrigger;
DiscoveredDriftTrigger(super.ownId, this.createTrigger);
}
typedef DiscoveredDriftTable = DiscoveredDriftElement<TableInducingStatement>;
typedef DiscoveredDriftView = DiscoveredDriftElement<CreateViewStatement>;
typedef DiscoveredDriftIndex = DiscoveredDriftElement<CreateIndexStatement>;
typedef DiscoveredDriftTrigger = DiscoveredDriftElement<CreateTriggerStatement>;
typedef DiscoveredDriftStatement = DiscoveredDriftElement<DeclaredStatement>;
abstract class DiscoveredDartElement<DE extends Element>
extends DiscoveredElement {

View File

@ -0,0 +1,32 @@
import 'element.dart';
/// A named SQL query defined in a `.drift` file. A later compile step will
/// further analyze this query and run analysis on it.
///
/// We deliberately only store very basic information here: The actual query
/// model is very complex and hard to serialize. Further, lots of generation
/// logic requires actual references to the AST which will be difficult to
/// translate across serialization run.
/// Since SQL queries only need to be fully analyzed before generation, and
/// since they are local elements which can't be referenced by others, there's
/// no clear advantage wrt. incremental compilation if queries are fully
/// analyzed and serialized. So, we just do this in the generator.
class DefinedSqlQuery extends DriftElement {
/// The unmodified source of the declared SQL statement forming this query.
final String sql;
/// The offset of [sql] in the source file, used to properly report errors
/// later.
final int sqlOffset;
@override
final List<DriftElement> references;
DefinedSqlQuery(
super.id,
super.declaration, {
required this.references,
required this.sql,
required this.sqlOffset,
});
}

View File

@ -2,6 +2,7 @@ export 'column.dart';
export 'dart.dart';
export 'element.dart';
export 'index.dart';
export 'query.dart';
export 'result_sets.dart';
export 'table.dart';
export 'trigger.dart';

View File

@ -48,6 +48,12 @@ class ElementSerializer {
'type': 'index',
'sql': element.createStmt,
};
} else if (element is DefinedSqlQuery) {
additionalInformation = {
'type': 'query',
'sql': element.sql,
'offset': element.sqlOffset,
};
} else if (element is DriftTrigger) {
additionalInformation = {
'type': 'trigger',
@ -345,6 +351,14 @@ abstract class ElementDeserializer {
table: references.whereType<DriftTable>().firstOrNull,
createStmt: json['sql'] as String,
);
case 'query':
return DefinedSqlQuery(
id,
declaration,
references: references,
sql: json['sql'] as String,
sqlOffset: json['offset'] as int,
);
case 'trigger':
return DriftTrigger(
id,

View File

@ -64,6 +64,24 @@ CREATE TABLE valid_2 (bar INTEGER);
'locallyDefinedElements', hasLength(2)));
});
test('warns about duplicate elements', () async {
final backend = TestBackend.inTest({
'a|lib/main.drift': '''
CREATE TABLE a (id INTEGER);
CREATE VIEW a AS VALUES(1,2,3);
''',
});
final state = await backend.driver
.prepareFileForAnalysis(Uri.parse('package:a/main.drift'));
expect(state.errorsDuringDiscovery, [
isDriftError(contains('already defines an element named `a`')),
]);
final result = state.discovery as DiscoveredDriftFile;
expect(result.locallyDefinedElements, [isA<DiscoveredDriftTable>()]);
});
group('imports', () {
test('are resolved', () async {
final backend = TestBackend.inTest({
@ -200,5 +218,38 @@ class InvalidGetter extends Table {
);
}
});
test('warns about duplicate elements', () async {
final backend = TestBackend.inTest({
'a|lib/a.dart': '''
import 'package:drift/drift.dart';
class A extends Table {
IntColumn get id => integer()();
String get tableName => 'tbl';
}
class B extends Table {
IntColumn get id => integer()();
String get tableName => 'tbl';
}
''',
});
final state = await backend.driver
.prepareFileForAnalysis(Uri.parse('package:a/a.dart'));
expect(state.errorsDuringDiscovery, [
isDriftError(contains('already defines an element named `tbl`')),
]);
final result = state.discovery as DiscoveredDartLibrary;
expect(result.locallyDefinedElements, [
isA<DiscoveredDartTable>()
.having((e) => e.dartElement.name, 'dartElement.name', 'A')
]);
});
});
}