mirror of https://github.com/AMT-Cheif/drift.git
New analyzer: Basic query support
This commit is contained in:
parent
e08ccdbcda
commit
2bd7596de6
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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')
|
||||
]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue