Merge remote-tracking branch 'upstream/develop' into column-name-case

This commit is contained in:
ValentinVignal 2022-12-03 14:56:51 +08:00
commit b08088ab1b
No known key found for this signature in database
GPG Key ID: 040FFDADFB7EF05A
38 changed files with 429 additions and 134 deletions

View File

@ -143,6 +143,37 @@ We currently support the following extensions:
module is available. Note that this is not the case for most sqlite3 builds,
including the ones shipping with `sqlite3_flutter_libs`.
### Known custom functions
The `modules` options can be used to tell drift's analyzer that a well-known
sqlite3 extension is available at runtime. In some backends (like a `NativeDatabase`),
it is also possible to specify entirely custom functions.
To be able to use these functions in `.drift` files, you can tell drift's
analyzer about them. To do so, add a `known_functions` block to the options:
```yaml
targets:
$default:
builders:
drift_dev:
options:
sql:
dialect: sqlite
options:
known_functions:
my_function: "boolean (text, int null)"
```
With these options, drift will analyze queries under the assumption that a SQL
function called `my_function` taking a non-nullable textual value an a nullable
integer will return a non-null value that drift can interpret as a boolean.
The syntax for a function type is defined as `<return type> (<argument types>)`.
Each type consists of an arbitrary word used to determine [column affinity](https://www.sqlite.org/datatype3.html#determination_of_column_affinity),
with drift also supporting `DATETIME` and `BOOLEAN` as type hints. Then, the
optional `NULL` keyword can be used to indicate whether the type is nullable.
## Recommended options
In general, we recommend using the default options.

View File

@ -8,7 +8,7 @@ environment:
dependencies:
drift:
path: ^1.8.2
json_annotation: ^4.6.0
json_annotation: ^4.7.0
docsy:
hosted: https://simonbinder.eu
version: ^0.2.2

View File

@ -148,22 +148,10 @@ class DriftCommunication {
/// [handler] returns a [Future], it will be awaited.
void setRequestHandler(dynamic Function(Request) handler) {
incomingRequests.listen((request) {
try {
final result = handler(request);
if (result is Future) {
result.then(
(value) => respond(request, value),
onError: (e, StackTrace s) {
respondError(request, e, s);
},
);
} else {
respond(request, result);
}
} catch (e, s) {
respondError(request, e, s);
}
Future.sync(() => handler(request)).then(
(result) => respond(request, result),
onError: (Object e, StackTrace s) => respondError(request, e, s),
);
});
}
}

View File

@ -83,7 +83,8 @@ mixin JsonTypeConverter<D, S> implements JsonTypeConverter2<D, S, S> {
/// Implementation for an enum to int converter that uses the index of the enum
/// as the value stored in the database.
class EnumIndexConverter<T extends Enum> extends TypeConverter<T, int> {
class EnumIndexConverter<T extends Enum> extends TypeConverter<T, int>
with JsonTypeConverter<T, int> {
/// All values of the enum.
final List<T> values;
@ -103,7 +104,8 @@ class EnumIndexConverter<T extends Enum> extends TypeConverter<T, int> {
/// Implementation for an enum to string converter that uses the name of the
/// enum as the value stored in the database.
class EnumNameConverter<T extends Enum> extends TypeConverter<T, String> {
class EnumNameConverter<T extends Enum> extends TypeConverter<T, String>
with JsonTypeConverter<T, String> {
/// All values of the enum.
final List<T> values;

View File

@ -548,8 +548,8 @@ class Config extends DataClass implements Insertable<Config> {
configKey: serializer.fromJson<String>(json['config_key']),
configValue: serializer.fromJson<String?>(json['config_value']),
syncState: serializer.fromJson<SyncType?>(json['sync_state']),
syncStateImplicit:
serializer.fromJson<SyncType?>(json['sync_state_implicit']),
syncStateImplicit: ConfigTable.$convertersyncStateImplicitn
.fromJson(serializer.fromJson<int?>(json['sync_state_implicit'])),
);
}
factory Config.fromJsonString(String encodedJson,
@ -563,7 +563,8 @@ class Config extends DataClass implements Insertable<Config> {
'config_key': serializer.toJson<String>(configKey),
'config_value': serializer.toJson<String?>(configValue),
'sync_state': serializer.toJson<SyncType?>(syncState),
'sync_state_implicit': serializer.toJson<SyncType?>(syncStateImplicit),
'sync_state_implicit': serializer.toJson<int?>(
ConfigTable.$convertersyncStateImplicitn.toJson(syncStateImplicit)),
};
}
@ -774,10 +775,11 @@ class ConfigTable extends Table with TableInfo<ConfigTable, Config> {
const SyncTypeConverter();
static TypeConverter<SyncType?, int?> $convertersyncStaten =
NullAwareTypeConverter.wrap($convertersyncState);
static TypeConverter<SyncType, int> $convertersyncStateImplicit =
static JsonTypeConverter2<SyncType, int, int> $convertersyncStateImplicit =
const EnumIndexConverter<SyncType>(SyncType.values);
static TypeConverter<SyncType?, int?> $convertersyncStateImplicitn =
NullAwareTypeConverter.wrap($convertersyncStateImplicit);
static JsonTypeConverter2<SyncType?, int?, int?>
$convertersyncStateImplicitn =
JsonTypeConverter2.asNullable($convertersyncStateImplicit);
@override
bool get isStrict => true;
@override
@ -1472,8 +1474,8 @@ class MyViewData extends DataClass {
configKey: serializer.fromJson<String>(json['config_key']),
configValue: serializer.fromJson<String?>(json['config_value']),
syncState: serializer.fromJson<SyncType?>(json['sync_state']),
syncStateImplicit:
serializer.fromJson<SyncType?>(json['sync_state_implicit']),
syncStateImplicit: ConfigTable.$convertersyncStateImplicitn
.fromJson(serializer.fromJson<int?>(json['sync_state_implicit'])),
);
}
factory MyViewData.fromJsonString(String encodedJson,
@ -1488,7 +1490,8 @@ class MyViewData extends DataClass {
'config_key': serializer.toJson<String>(configKey),
'config_value': serializer.toJson<String?>(configValue),
'sync_state': serializer.toJson<SyncType?>(syncState),
'sync_state_implicit': serializer.toJson<SyncType?>(syncStateImplicit),
'sync_state_implicit': serializer.toJson<int?>(
ConfigTable.$convertersyncStateImplicitn.toJson(syncStateImplicit)),
};
}

View File

@ -39,7 +39,8 @@ class Category extends DataClass implements Insertable<Category> {
return Category(
id: serializer.fromJson<int>(json['id']),
description: serializer.fromJson<String>(json['description']),
priority: serializer.fromJson<CategoryPriority>(json['priority']),
priority: $CategoriesTable.$converterpriority
.fromJson(serializer.fromJson<int>(json['priority'])),
descriptionInUpperCase:
serializer.fromJson<String>(json['descriptionInUpperCase']),
);
@ -55,7 +56,8 @@ class Category extends DataClass implements Insertable<Category> {
return <String, dynamic>{
'id': serializer.toJson<int>(id),
'description': serializer.toJson<String>(description),
'priority': serializer.toJson<CategoryPriority>(priority),
'priority': serializer
.toJson<int>($CategoriesTable.$converterpriority.toJson(priority)),
'descriptionInUpperCase':
serializer.toJson<String>(descriptionInUpperCase),
};
@ -256,7 +258,7 @@ class $CategoriesTable extends Categories
return $CategoriesTable(attachedDatabase, alias);
}
static TypeConverter<CategoryPriority, int> $converterpriority =
static JsonTypeConverter2<CategoryPriority, int, int> $converterpriority =
const EnumIndexConverter<CategoryPriority>(CategoryPriority.values);
}
@ -321,7 +323,8 @@ class TodoEntry extends DataClass implements Insertable<TodoEntry> {
content: serializer.fromJson<String>(json['content']),
targetDate: serializer.fromJson<DateTime?>(json['target_date']),
category: serializer.fromJson<int?>(json['category']),
status: serializer.fromJson<TodoStatus?>(json['status']),
status: $TodosTableTable.$converterstatusn
.fromJson(serializer.fromJson<String?>(json['status'])),
);
}
factory TodoEntry.fromJsonString(String encodedJson,
@ -338,7 +341,8 @@ class TodoEntry extends DataClass implements Insertable<TodoEntry> {
'content': serializer.toJson<String>(content),
'target_date': serializer.toJson<DateTime?>(targetDate),
'category': serializer.toJson<int?>(category),
'status': serializer.toJson<TodoStatus?>(status),
'status': serializer
.toJson<String?>($TodosTableTable.$converterstatusn.toJson(status)),
};
}
@ -605,10 +609,10 @@ class $TodosTableTable extends TodosTable
return $TodosTableTable(attachedDatabase, alias);
}
static TypeConverter<TodoStatus, String> $converterstatus =
static JsonTypeConverter2<TodoStatus, String, String> $converterstatus =
const EnumNameConverter<TodoStatus>(TodoStatus.values);
static TypeConverter<TodoStatus?, String?> $converterstatusn =
NullAwareTypeConverter.wrap($converterstatus);
static JsonTypeConverter2<TodoStatus?, String?, String?> $converterstatusn =
JsonTypeConverter2.asNullable($converterstatus);
}
class User extends DataClass implements Insertable<User> {
@ -1308,7 +1312,7 @@ class PureDefault extends DataClass implements Insertable<PureDefault> {
serializer ??= driftRuntimeOptions.defaultSerializer;
return PureDefault(
txt: $PureDefaultsTable.$convertertxtn
.fromJson(serializer.fromJson<Map<dynamic, dynamic>>(json['txt'])),
.fromJson(serializer.fromJson<Map<dynamic, dynamic>?>(json['txt'])),
);
}
factory PureDefault.fromJsonString(String encodedJson,

View File

@ -20,7 +20,7 @@ final Map<String, dynamic> _regularSerialized = {
'content': 'content',
'target_date': _someDate.millisecondsSinceEpoch,
'category': 3,
'status': TodoStatus.open,
'status': TodoStatus.open.name,
};
final Map<String, dynamic> _asTextSerialized = {
@ -29,7 +29,7 @@ final Map<String, dynamic> _asTextSerialized = {
'content': 'content',
'target_date': _someDate.toIso8601String(),
'category': 3,
'status': TodoStatus.open,
'status': TodoStatus.open.name,
};
final Map<String, dynamic> _customSerialized = {
@ -38,7 +38,7 @@ final Map<String, dynamic> _customSerialized = {
'content': 'content',
'target_date': _someDate.toIso8601String(),
'category': 3,
'status': TodoStatus.open,
'status': TodoStatus.open.name,
};
class CustomSerializer extends ValueSerializer {

View File

@ -9,6 +9,7 @@ import '../resolver/dart/helper.dart';
import '../resolver/discover.dart';
import '../resolver/drift/sqlparser/mapping.dart';
import '../resolver/file_analysis.dart';
import '../resolver/queries/custom_known_functions.dart';
import '../resolver/resolver.dart';
import '../results/results.dart';
import '../serializer.dart';
@ -69,6 +70,7 @@ class DriftAnalysisDriver {
EngineOptions(
useDriftExtensions: true,
enabledExtensions: [
DriftOptionsExtension(options),
if (options.hasModule(SqlModule.fts5)) const Fts5Extension(),
if (options.hasModule(SqlModule.json1)) const Json1Extension(),
if (options.hasModule(SqlModule.moor_ffi))

View File

@ -1,7 +1,10 @@
import 'package:charcode/ascii.dart';
import 'package:drift/drift.dart' show SqlDialect;
import 'package:json_annotation/json_annotation.dart';
import 'package:meta/meta.dart';
import 'package:sqlparser/sqlparser.dart' show SqliteVersion;
import 'package:sqlparser/sqlparser.dart'
show BasicType, ResolvedType, SchemaFromCreateTable, SqliteVersion;
import 'package:string_scanner/string_scanner.dart';
part '../generated/analysis/options.g.dart';
@ -206,7 +209,13 @@ class SqliteAnalysisOptions {
@_SqliteVersionConverter()
final SqliteVersion? version;
const SqliteAnalysisOptions({this.modules = const [], this.version});
final Map<String, KnownSqliteFunction> knownFunctions;
const SqliteAnalysisOptions({
this.modules = const [],
this.version,
this.knownFunctions = const {},
});
factory SqliteAnalysisOptions.fromJson(Map json) {
return _$SqliteAnalysisOptionsFromJson(json);
@ -215,6 +224,73 @@ class SqliteAnalysisOptions {
Map<String, Object?> toJson() => _$SqliteAnalysisOptionsToJson(this);
}
class KnownSqliteFunction {
final List<ResolvedType> argumentTypes;
final ResolvedType returnType;
KnownSqliteFunction(this.argumentTypes, this.returnType);
factory KnownSqliteFunction.fromJson(String json) {
final scanner = StringScanner(json);
final types = SchemaFromCreateTable(driftExtensions: true);
ResolvedType parseType() {
scanner.scan(_whitespace);
scanner.expect(_word, name: 'Type name');
final type = types.resolveColumnType(scanner.lastMatch?.group(0));
return type.copyWith(nullable: scanner.scan(_null));
}
final argumentTypes = <ResolvedType>[];
final returnType = parseType();
scanner
..scan(_whitespace)
..expectChar($openParen)
..scan(_whitespace);
if (scanner.peekChar() != $closeParen) {
argumentTypes.add(parseType());
while (scanner.scanChar($comma)) {
argumentTypes.add(parseType());
}
}
scanner
..scan(_whitespace)
..expectChar($closeParen)
..scan(_whitespace)
..expectDone();
return KnownSqliteFunction(argumentTypes, returnType);
}
String toJson() {
String toString(ResolvedType type) {
switch (type.type!) {
case BasicType.nullType:
return 'NULL';
case BasicType.int:
return 'INTEGER';
case BasicType.real:
return 'REAL';
case BasicType.text:
return 'TEXT';
case BasicType.blob:
return 'BLOB';
}
}
final types = argumentTypes.map(toString).join(', ');
return '${toString(returnType)}($types)';
}
static final _word = RegExp(r'\w+');
static final _null = RegExp(r'\s+null', caseSensitive: false);
static final _whitespace = RegExp(r'\s*');
}
class _SqliteVersionConverter extends JsonConverter<SqliteVersion, String> {
static final _versionRegex = RegExp(r'(\d+)\.(\d+)');

View File

@ -341,6 +341,7 @@ class ColumnParser {
_resolver.resolver.driver.options.columnNameCase
.apply(getter.name.lexeme);
final sqlType = _startMethodToColumnType(foundStartMethod);
final helper = await _resolver.resolver.driver.loadKnownTypes();
AppliedTypeConverter? converter;
if (mappedAs != null) {
@ -351,7 +352,7 @@ class ColumnParser {
nullable,
(message) => _resolver.reportError(
DriftAnalysisError.inDartAst(element, mappedAs!, message)),
await _resolver.resolver.driver.loadKnownTypes(),
helper,
);
}
@ -370,6 +371,7 @@ class ColumnParser {
remainingExpr.typeArguments ?? remainingExpr.methodName, msg)),
enumType,
EnumType.intEnum,
helper,
);
} else if (foundStartMethod == _startTextEnum) {
if (converter != null) {
@ -386,6 +388,7 @@ class ColumnParser {
remainingExpr.typeArguments ?? remainingExpr.methodName, msg)),
enumType,
EnumType.textEnum,
helper,
);
}

View File

@ -28,6 +28,7 @@ class DriftTableResolver extends LocalElementResolver<DiscoveredDriftTable> {
Table table;
final references = <DriftElement>{};
final stmt = discovered.sqlNode;
final helper = await resolver.driver.loadKnownTypes();
try {
final reader = SchemaFromCreateTable(
@ -74,6 +75,7 @@ class DriftTableResolver extends LocalElementResolver<DiscoveredDriftTable> {
DriftAnalysisError.inDriftFile(column.definition ?? stmt, msg)),
dartClass.classElement.thisType,
type == DriftSqlType.int ? EnumType.intEnum : EnumType.textEnum,
helper,
);
}
}

View File

@ -0,0 +1,60 @@
import 'package:sqlparser/sqlparser.dart';
import '../../options.dart';
class DriftOptionsExtension implements Extension {
final DriftOptions options;
DriftOptionsExtension(this.options);
@override
void register(SqlEngine engine) {
final knownFunctions = options.sqliteAnalysisOptions?.knownFunctions;
if (knownFunctions != null) {
engine.registerFunctionHandler(_CustomFunctions(knownFunctions));
}
}
}
class _CustomFunctions extends FunctionHandler {
// always has lowercase keys
final Map<String, KnownSqliteFunction> _functions;
_CustomFunctions(Map<String, KnownSqliteFunction> functions)
: _functions = {
for (final function in functions.entries)
function.key.toLowerCase(): function.value,
};
@override
late final Set<String> functionNames = _functions.keys.toSet();
@override
ResolveResult inferArgumentType(
AnalysisContext context, SqlInvocation call, Expression argument) {
final types = _functions[call.name.toLowerCase()]?.argumentTypes;
if (types == null) {
return const ResolveResult.unknown();
}
final parameters = call.parameters;
if (parameters is ExprFunctionParameters) {
final index = parameters.parameters.indexOf(argument);
if (index < types.length) {
return ResolveResult(types[index]);
}
}
return const ResolveResult.unknown();
}
@override
ResolveResult inferReturnType(AnalysisContext context, SqlInvocation call,
List<Typeable> expandedArgs) {
final type = _functions[call.name.toLowerCase()]?.returnType;
return type != null ? ResolveResult(type) : const ResolveResult.unknown();
}
}

View File

@ -230,7 +230,9 @@ AppliedTypeConverter readEnumConverter(
void Function(String) reportError,
DartType dartEnumType,
EnumType columnEnumType,
KnownDriftTypes helper,
) {
final typeProvider = helper.helperLibrary.typeProvider;
if (dartEnumType is! InterfaceType) {
reportError('Not a class: `$dartEnumType`');
}
@ -261,7 +263,9 @@ AppliedTypeConverter readEnumConverter(
return AppliedTypeConverter(
expression: expression,
dartType: dartEnumType,
jsonType: null,
jsonType: columnEnumType == EnumType.intEnum
? typeProvider.intType
: typeProvider.stringType,
sqlType: columnEnumType == EnumType.intEnum
? DriftSqlType.int
: DriftSqlType.string,

View File

@ -201,7 +201,7 @@ SqliteAnalysisOptions _$SqliteAnalysisOptionsFromJson(Map json) =>
($checkedConvert) {
$checkKeys(
json,
allowedKeys: const ['modules', 'version'],
allowedKeys: const ['modules', 'version', 'known_functions'],
);
final val = SqliteAnalysisOptions(
modules: $checkedConvert(
@ -215,9 +215,18 @@ SqliteAnalysisOptions _$SqliteAnalysisOptionsFromJson(Map json) =>
'version',
(v) => _$JsonConverterFromJson<String, SqliteVersion>(
v, const _SqliteVersionConverter().fromJson)),
knownFunctions: $checkedConvert(
'known_functions',
(v) =>
(v as Map?)?.map(
(k, e) => MapEntry(
k as String, KnownSqliteFunction.fromJson(e as String)),
) ??
const {}),
);
return val;
},
fieldKeyMap: const {'knownFunctions': 'known_functions'},
);
Map<String, dynamic> _$SqliteAnalysisOptionsToJson(
@ -226,6 +235,8 @@ Map<String, dynamic> _$SqliteAnalysisOptionsToJson(
'modules': instance.modules.map((e) => _$SqlModuleEnumMap[e]!).toList(),
'version': _$JsonConverterToJson<String, SqliteVersion>(
instance.version, const _SqliteVersionConverter().toJson),
'known_functions':
instance.knownFunctions.map((k, e) => MapEntry(k, e.toJson())),
};
Value? _$JsonConverterFromJson<Json, Value>(

View File

@ -62,11 +62,10 @@ class DatabaseWriter {
..write('class $className extends ')
..writeDriftRef('GeneratedDatabase')
..writeln('{')
..writeln(
'$className(${firstLeaf.refDrift('QueryExecutor e')}): super(e);');
..writeln('$className(${firstLeaf.drift('QueryExecutor e')}): super(e);');
if (dbScope.options.generateConnectConstructor) {
final conn = firstLeaf.refDrift('DatabaseConnection');
final conn = firstLeaf.drift('DatabaseConnection');
firstLeaf.write('$className.connect($conn c): super.connect(c); \n');
}
@ -220,7 +219,7 @@ class DatabaseWriter {
if (scope.options.storeDateTimeValuesAsText) {
// Override database options to reflect that DateTimes are stored as text.
final options = schemaScope.refDrift('DriftDatabaseOptions');
final options = schemaScope.drift('DriftDatabaseOptions');
schemaScope
..writeln('@override')

View File

@ -664,14 +664,18 @@ class _ExpandedVariableWriter {
String constructVar(String dartExpr) {
// No longer an array here, we apply a for loop if necessary
final type = element.innerColumnType(nullable: false);
final buffer = StringBuffer('Variable<$type>(');
final varType = _emitter.drift('Variable');
final buffer = StringBuffer('$varType<$type>(');
final capture = element.forCaptured;
final converter = element.typeConverter;
if (converter != null) {
// Apply the converter.
if (element.nullable && converter.canBeSkippedForNulls) {
buffer.write('NullAwareTypeConverter.wrapToSql('
final nullAware = _emitter.drift('NullAwareTypeConverter');
buffer.write('$nullAware.wrapToSql('
'${_converter(_emitter, element.typeConverter!)}, $dartExpr)');
} else {
buffer.write(

View File

@ -133,8 +133,11 @@ class DataClassWriter {
final typeConverter = column.typeConverter;
if (typeConverter != null && typeConverter.alsoAppliesToJsonConversion) {
final type =
var type =
_emitter.dartCode(AnnotatedDartCode.type(typeConverter.jsonType!));
if (column.nullable) {
type = '$type?';
}
final fromConverter = "serializer.fromJson<$type>(json['$jsonKey'])";
final converterField = _converter(column);

View File

@ -7,8 +7,6 @@ import '../analysis/options.dart';
import 'import_manager.dart';
import 'queries/sql_writer.dart';
Uri _driftImport = Uri.parse('package:drift/drift.dart');
/// Manages a tree structure which we use to generate code.
///
/// Each leaf in the tree is a [StringBuffer] that contains some code. A
@ -287,9 +285,7 @@ class TextEmitter extends _Node {
return write(refUri(definition, element));
}
void writeDriftRef(String element) => write(refDrift(element));
String refDrift(String element) => refUri(_driftImport, element);
void writeDriftRef(String element) => write(drift(element));
void writeDart(AnnotatedDartCode code) => write(dartCode(code));

View File

@ -14,7 +14,7 @@ dependencies:
recase: '>=2.0.1 <5.0.0'
meta: ^1.1.0
path: ^1.6.0
json_annotation: ^4.6.0
json_annotation: ^4.7.0
stream_transform: '>=0.1.0 <3.0.0'
# CLI
@ -42,6 +42,7 @@ dependencies:
build_config: '>=0.3.1 <2.0.0'
dart_style: '>=1.3.3 <3.0.0'
source_gen: '>=0.9.4 <2.0.0'
string_scanner: ^1.2.0
dev_dependencies:
lints: ^2.0.0

View File

@ -86,4 +86,34 @@ sqlite:
.withSpan('place_spellfix'),
]);
});
group('parses functions', () {
test('succesfully', () {
final function = KnownSqliteFunction.fromJson('text (int, boolean nUlL)');
expect(function.returnType.type, BasicType.text);
expect(function.argumentTypes, [
isA<ResolvedType>().having((e) => e.type, 'type', BasicType.int),
isA<ResolvedType>()
.having((e) => e.type, 'type', BasicType.int)
.having((e) => e.hint, 'hint', const IsBoolean())
.having((e) => e.nullable, 'nullable', true),
]);
});
test('supports empty args', () {
final function = KnownSqliteFunction.fromJson('text ()');
expect(function.returnType.type, BasicType.text);
expect(function.argumentTypes, isEmpty);
});
test('fails for invalid syntax', () {
final throws = throwsFormatException;
expect(() => KnownSqliteFunction.fromJson('x'), throws);
expect(() => KnownSqliteFunction.fromJson('(boolean)'), throws);
expect(() => KnownSqliteFunction.fromJson('int (boolean, )'), throws);
});
});
}

View File

@ -1,7 +1,7 @@
import 'package:drift/drift.dart' show DriftSqlType;
import 'package:drift_dev/src/analysis/results/results.dart';
import 'package:drift_dev/src/analysis/options.dart';
import 'package:sqlparser/sqlparser.dart';
import 'package:sqlparser/sqlparser.dart' hide ResultColumn;
import 'package:test/test.dart';
import '../../test_utils.dart';
@ -257,4 +257,36 @@ LEFT JOIN tableB1 AS tableB2 -- nullable
[false, true, false, true],
);
});
test('supports custom functions', () async {
final withoutOptions =
TestBackend.inTest({'a|lib/a.drift': 'a: SELECT my_function();'});
var result = await withoutOptions.analyze('package:a/a.drift');
expect(result.allErrors, [
isDriftError('Function my_function could not be found')
.withSpan('my_function'),
isDriftError(startsWith('Expression has an unknown type'))
.withSpan('my_function()'),
]);
final withOptions =
TestBackend.inTest({'a|lib/a.drift': 'a: SELECT my_function(?, ?);'},
options: DriftOptions.defaults(
sqliteAnalysisOptions: SqliteAnalysisOptions(knownFunctions: {
'my_function':
KnownSqliteFunction.fromJson('boolean (int, text)')
}),
));
result = await withOptions.analyze('package:a/a.drift');
withOptions.expectNoErrors();
final query = result.fileAnalysis!.resolvedQueries.values.single;
expect(query.resultSet!.columns, [
isA<ResultColumn>().having((e) => e.sqlType, 'sqlType', DriftSqlType.bool)
]);
final args = query.variables;
expect(args.map((e) => e.sqlType), [DriftSqlType.int, DriftSqlType.string]);
});
}

View File

@ -117,7 +117,7 @@ class Database extends _$Database {}
checkOutputs({
'a|lib/main.drift.dart': decodedMatches(contains(r'''
static TypeConverter<Priority, String> $converterpriority =
static JsonTypeConverter2<Priority, String, String> $converterpriority =
const EnumNameConverter<Priority>(Priority.values);''')),
}, writer.dartOutputs, writer);
},

View File

@ -142,7 +142,7 @@ class $CategoriesTable extends Categories
final GeneratedDatabase attachedDatabase;
final String? _alias;
$CategoriesTable(this.attachedDatabase, [this._alias]);
final VerificationMeta _idMeta = const VerificationMeta('id');
static const VerificationMeta _idMeta = const VerificationMeta('id');
@override
late final GeneratedColumn<int> id = GeneratedColumn<int>(
'id', aliasedName, false,
@ -151,12 +151,12 @@ class $CategoriesTable extends Categories
requiredDuringInsert: false,
defaultConstraints:
GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
final VerificationMeta _nameMeta = const VerificationMeta('name');
static const VerificationMeta _nameMeta = const VerificationMeta('name');
@override
late final GeneratedColumn<String> name = GeneratedColumn<String>(
'name', aliasedName, false,
type: DriftSqlType.string, requiredDuringInsert: true);
final VerificationMeta _colorMeta = const VerificationMeta('color');
static const VerificationMeta _colorMeta = const VerificationMeta('color');
@override
late final GeneratedColumnWithTypeConverter<Color, int> color =
GeneratedColumn<int>('color', aliasedName, false,
@ -382,7 +382,7 @@ class $TodoEntriesTable extends TodoEntries
final GeneratedDatabase attachedDatabase;
final String? _alias;
$TodoEntriesTable(this.attachedDatabase, [this._alias]);
final VerificationMeta _idMeta = const VerificationMeta('id');
static const VerificationMeta _idMeta = const VerificationMeta('id');
@override
late final GeneratedColumn<int> id = GeneratedColumn<int>(
'id', aliasedName, false,
@ -391,13 +391,14 @@ class $TodoEntriesTable extends TodoEntries
requiredDuringInsert: false,
defaultConstraints:
GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
final VerificationMeta _descriptionMeta =
static const VerificationMeta _descriptionMeta =
const VerificationMeta('description');
@override
late final GeneratedColumn<String> description = GeneratedColumn<String>(
'description', aliasedName, false,
type: DriftSqlType.string, requiredDuringInsert: true);
final VerificationMeta _categoryMeta = const VerificationMeta('category');
static const VerificationMeta _categoryMeta =
const VerificationMeta('category');
@override
late final GeneratedColumn<int> category = GeneratedColumn<int>(
'category', aliasedName, true,
@ -405,7 +406,8 @@ class $TodoEntriesTable extends TodoEntries
requiredDuringInsert: false,
defaultConstraints:
GeneratedColumn.constraintIsAlways('REFERENCES categories (id)'));
final VerificationMeta _dueDateMeta = const VerificationMeta('dueDate');
static const VerificationMeta _dueDateMeta =
const VerificationMeta('dueDate');
@override
late final GeneratedColumn<DateTime> dueDate = GeneratedColumn<DateTime>(
'due_date', aliasedName, true,
@ -564,7 +566,7 @@ class TextEntries extends Table
final GeneratedDatabase attachedDatabase;
final String? _alias;
TextEntries(this.attachedDatabase, [this._alias]);
final VerificationMeta _descriptionMeta =
static const VerificationMeta _descriptionMeta =
const VerificationMeta('description');
late final GeneratedColumn<String> description = GeneratedColumn<String>(
'description', aliasedName, false,
@ -594,7 +596,7 @@ class TextEntries extends Table
}
@override
Set<GeneratedColumn> get $primaryKey => const <GeneratedColumn>{};
Set<GeneratedColumn> get $primaryKey => const {};
@override
TextEntrie map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
@ -670,7 +672,7 @@ abstract class _$AppDatabase extends GeneratedDatabase {
}
@override
Iterable<TableInfo<Table, dynamic>> get allTables =>
Iterable<TableInfo<Table, Object?>> get allTables =>
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
@override
List<DatabaseSchemaEntity> get allSchemaEntities => [

View File

@ -115,7 +115,7 @@ class $NotesTable extends Notes with TableInfo<$NotesTable, Note> {
final GeneratedDatabase attachedDatabase;
final String? _alias;
$NotesTable(this.attachedDatabase, [this._alias]);
final VerificationMeta _idMeta = const VerificationMeta('id');
static const VerificationMeta _idMeta = const VerificationMeta('id');
@override
late final GeneratedColumn<int> id = GeneratedColumn<int>(
'id', aliasedName, false,
@ -124,7 +124,8 @@ class $NotesTable extends Notes with TableInfo<$NotesTable, Note> {
requiredDuringInsert: false,
defaultConstraints:
GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
final VerificationMeta _contentMeta = const VerificationMeta('content');
static const VerificationMeta _contentMeta =
const VerificationMeta('content');
@override
late final GeneratedColumn<String> content = GeneratedColumn<String>(
'content', aliasedName, false,
@ -175,7 +176,7 @@ abstract class _$MyEncryptedDatabase extends GeneratedDatabase {
_$MyEncryptedDatabase(QueryExecutor e) : super(e);
late final $NotesTable notes = $NotesTable(this);
@override
Iterable<TableInfo<Table, dynamic>> get allTables =>
Iterable<TableInfo<Table, Object?>> get allTables =>
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
@override
List<DatabaseSchemaEntity> get allSchemaEntities => [notes];

View File

@ -115,13 +115,13 @@ class Entries extends Table with TableInfo<Entries, Entrie> {
final GeneratedDatabase attachedDatabase;
final String? _alias;
Entries(this.attachedDatabase, [this._alias]);
final VerificationMeta _idMeta = const VerificationMeta('id');
static const VerificationMeta _idMeta = const VerificationMeta('id');
late final GeneratedColumn<int> id = GeneratedColumn<int>(
'id', aliasedName, false,
type: DriftSqlType.int,
requiredDuringInsert: false,
$customConstraints: 'PRIMARY KEY');
final VerificationMeta _valueMeta = const VerificationMeta('value');
static const VerificationMeta _valueMeta = const VerificationMeta('value');
late final GeneratedColumn<String> value = GeneratedColumn<String>(
'text', aliasedName, false,
type: DriftSqlType.string,
@ -201,7 +201,7 @@ abstract class _$MyDatabase extends GeneratedDatabase {
}
@override
Iterable<TableInfo<Table, dynamic>> get allTables =>
Iterable<TableInfo<Table, Object?>> get allTables =>
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
@override
List<DatabaseSchemaEntity> get allSchemaEntities => [entries];

View File

@ -823,7 +823,7 @@ class GroupCount extends ViewInfo<GroupCount, GroupCountData>
String get entityName => 'group_count';
@override
String get createViewStmt =>
'CREATE VIEW group_count AS SELECT\n users.*,\n (SELECT COUNT(*) FROM "groups" WHERE owner = users.id) AS group_count\n FROM users;';
'CREATE VIEW group_count AS SELECT users.*, (SELECT COUNT(*) FROM "groups" WHERE owner = users.id) AS group_count FROM users';
@override
GroupCount get asDslTable => this;
@override

View File

@ -7,6 +7,10 @@ CREATE VIRTUAL TABLE search_in_posts USING fts5 (
content_rowid=id
);
search: WITH relevant_ports AS (SELECT rowid FROM search_in_posts WHERE search_in_posts MATCH ?)
SELECT posts.* FROM relevant_ports results
INNER JOIN posts ON id = results.rowid;
-- Keep fts5 table and posts synchronized
CREATE TRIGGER posts_insert AFTER INSERT ON posts BEGIN

View File

@ -205,5 +205,20 @@ i0.Trigger get postsDelete => i0.Trigger(
class SearchDrift extends i2.ModularAccessor {
SearchDrift(i0.GeneratedDatabase db) : super(db);
i0.Selectable<i3.Post> search(String var1) {
return customSelect(
'WITH relevant_ports AS (SELECT "rowid" FROM search_in_posts WHERE search_in_posts MATCH ?1) SELECT posts.* FROM relevant_ports AS results INNER JOIN posts ON id = results."rowid"',
variables: [
i0.Variable<String>(var1)
],
readsFrom: {
searchInPosts,
posts,
}).asyncMap(posts.mapFromRow);
}
i1.SearchInPosts get searchInPosts =>
this.resultSet<i1.SearchInPosts>('search_in_posts');
i3.Posts get posts => this.resultSet<i3.Posts>('posts');
i3.PostsDrift get postsDrift => this.accessor(i3.PostsDrift.new);
}

View File

@ -115,13 +115,13 @@ class Entries extends Table with TableInfo<Entries, Entrie> {
final GeneratedDatabase attachedDatabase;
final String? _alias;
Entries(this.attachedDatabase, [this._alias]);
final VerificationMeta _idMeta = const VerificationMeta('id');
static const VerificationMeta _idMeta = const VerificationMeta('id');
late final GeneratedColumn<int> id = GeneratedColumn<int>(
'id', aliasedName, false,
type: DriftSqlType.int,
requiredDuringInsert: false,
$customConstraints: 'PRIMARY KEY');
final VerificationMeta _valueMeta = const VerificationMeta('value');
static const VerificationMeta _valueMeta = const VerificationMeta('value');
late final GeneratedColumn<String> value = GeneratedColumn<String>(
'text', aliasedName, false,
type: DriftSqlType.string,
@ -202,7 +202,7 @@ abstract class _$MyDatabase extends GeneratedDatabase {
}
@override
Iterable<TableInfo<Table, dynamic>> get allTables =>
Iterable<TableInfo<Table, Object?>> get allTables =>
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
@override
List<DatabaseSchemaEntity> get allSchemaEntities => [entries];

View File

@ -117,12 +117,12 @@ class $KeyValuesTable extends KeyValues
final GeneratedDatabase attachedDatabase;
final String? _alias;
$KeyValuesTable(this.attachedDatabase, [this._alias]);
final VerificationMeta _keyMeta = const VerificationMeta('key');
static const VerificationMeta _keyMeta = const VerificationMeta('key');
@override
late final GeneratedColumn<String> key = GeneratedColumn<String>(
'key', aliasedName, false,
type: DriftSqlType.string, requiredDuringInsert: true);
final VerificationMeta _valueMeta = const VerificationMeta('value');
static const VerificationMeta _valueMeta = const VerificationMeta('value');
@override
late final GeneratedColumn<String> value = GeneratedColumn<String>(
'value', aliasedName, false,
@ -176,7 +176,7 @@ abstract class _$Database extends GeneratedDatabase {
_$Database(QueryExecutor e) : super(e);
late final $KeyValuesTable keyValues = $KeyValuesTable(this);
@override
Iterable<TableInfo<Table, dynamic>> get allTables =>
Iterable<TableInfo<Table, Object?>> get allTables =>
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
@override
List<DatabaseSchemaEntity> get allSchemaEntities => [keyValues];

View File

@ -210,7 +210,7 @@ class $UsersTable extends Users with TableInfo<$UsersTable, User> {
final GeneratedDatabase attachedDatabase;
final String? _alias;
$UsersTable(this.attachedDatabase, [this._alias]);
final VerificationMeta _idMeta = const VerificationMeta('id');
static const VerificationMeta _idMeta = const VerificationMeta('id');
@override
late final GeneratedColumn<int> id = GeneratedColumn<int>(
'id', aliasedName, false,
@ -219,23 +219,24 @@ class $UsersTable extends Users with TableInfo<$UsersTable, User> {
requiredDuringInsert: false,
defaultConstraints:
GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
final VerificationMeta _nameMeta = const VerificationMeta('name');
static const VerificationMeta _nameMeta = const VerificationMeta('name');
@override
late final GeneratedColumn<String> name = GeneratedColumn<String>(
'name', aliasedName, false,
type: DriftSqlType.string, requiredDuringInsert: true);
final VerificationMeta _birthDateMeta = const VerificationMeta('birthDate');
static const VerificationMeta _birthDateMeta =
const VerificationMeta('birthDate');
@override
late final GeneratedColumn<DateTime> birthDate = GeneratedColumn<DateTime>(
'birth_date', aliasedName, false,
type: DriftSqlType.dateTime, requiredDuringInsert: true);
final VerificationMeta _profilePictureMeta =
static const VerificationMeta _profilePictureMeta =
const VerificationMeta('profilePicture');
@override
late final GeneratedColumn<Uint8List> profilePicture =
GeneratedColumn<Uint8List>('profile_picture', aliasedName, true,
type: DriftSqlType.blob, requiredDuringInsert: false);
final VerificationMeta _preferencesMeta =
static const VerificationMeta _preferencesMeta =
const VerificationMeta('preferences');
@override
late final GeneratedColumnWithTypeConverter<Preferences?, String>
@ -455,17 +456,19 @@ class $FriendshipsTable extends Friendships
final GeneratedDatabase attachedDatabase;
final String? _alias;
$FriendshipsTable(this.attachedDatabase, [this._alias]);
final VerificationMeta _firstUserMeta = const VerificationMeta('firstUser');
static const VerificationMeta _firstUserMeta =
const VerificationMeta('firstUser');
@override
late final GeneratedColumn<int> firstUser = GeneratedColumn<int>(
'first_user', aliasedName, false,
type: DriftSqlType.int, requiredDuringInsert: true);
final VerificationMeta _secondUserMeta = const VerificationMeta('secondUser');
static const VerificationMeta _secondUserMeta =
const VerificationMeta('secondUser');
@override
late final GeneratedColumn<int> secondUser = GeneratedColumn<int>(
'second_user', aliasedName, false,
type: DriftSqlType.int, requiredDuringInsert: true);
final VerificationMeta _reallyGoodFriendsMeta =
static const VerificationMeta _reallyGoodFriendsMeta =
const VerificationMeta('reallyGoodFriends');
@override
late final GeneratedColumn<bool> reallyGoodFriends =
@ -625,7 +628,7 @@ abstract class _$Database extends GeneratedDatabase {
}
@override
Iterable<TableInfo<Table, dynamic>> get allTables =>
Iterable<TableInfo<Table, Object?>> get allTables =>
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
@override
List<DatabaseSchemaEntity> get allSchemaEntities => [users, friendships];

View File

@ -97,7 +97,7 @@ class $FoosTable extends Foos with TableInfo<$FoosTable, Foo> {
final GeneratedDatabase attachedDatabase;
final String? _alias;
$FoosTable(this.attachedDatabase, [this._alias]);
final VerificationMeta _idMeta = const VerificationMeta('id');
static const VerificationMeta _idMeta = const VerificationMeta('id');
@override
late final GeneratedColumn<int> id = GeneratedColumn<int>(
'id', aliasedName, false,
@ -234,7 +234,7 @@ class $BarsTable extends Bars with TableInfo<$BarsTable, Bar> {
final GeneratedDatabase attachedDatabase;
final String? _alias;
$BarsTable(this.attachedDatabase, [this._alias]);
final VerificationMeta _idMeta = const VerificationMeta('id');
static const VerificationMeta _idMeta = const VerificationMeta('id');
@override
late final GeneratedColumn<int> id = GeneratedColumn<int>(
'id', aliasedName, false,
@ -282,7 +282,7 @@ abstract class _$_FakeDb extends GeneratedDatabase {
late final $FoosTable foos = $FoosTable(this);
late final $BarsTable bars = $BarsTable(this);
@override
Iterable<TableInfo<Table, dynamic>> get allTables =>
Iterable<TableInfo<Table, Object?>> get allTables =>
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
@override
List<DatabaseSchemaEntity> get allSchemaEntities => [foos, bars];

View File

@ -601,7 +601,7 @@ class TypeResolver extends RecursiveVisitor<TypeExpectation, void> {
session.context.reportError(AnalysisError(
type: AnalysisErrorType.unknownFunction,
message: 'Function ${e.name} could not be found',
relevantNode: e,
relevantNode: e.nameToken ?? e,
));
return null;
}

View File

@ -217,6 +217,9 @@ class TableValuedFunction extends Queryable
@override
final String name;
@override
Token? nameToken;
@override
final String? as;

View File

@ -7,6 +7,9 @@ class AggregateFunctionInvocation extends Expression
@override
String get name => function.identifier;
@override
Token? nameToken;
@override
FunctionParameters parameters;
Expression? filter;
@ -18,7 +21,7 @@ class AggregateFunctionInvocation extends Expression
required this.function,
required this.parameters,
this.filter,
});
}) : nameToken = function;
@override
R accept<A, R>(AstVisitor<A, R> visitor, A arg) {

View File

@ -10,6 +10,8 @@ abstract class SqlInvocation implements AstNode {
/// The name of the function being called
String get name;
Token? nameToken;
FunctionParameters get parameters;
}
@ -24,6 +26,9 @@ class FunctionExpression extends Expression
@override
FunctionParameters parameters;
@override
Token? nameToken;
FunctionExpression({required this.name, required this.parameters});
@override

View File

@ -33,9 +33,6 @@ class Parser {
final List<ParsingError> errors = [];
final AutoCompleteEngine? autoComplete;
// todo remove this and don't be that lazy in driftFile()
var _lastStmtHadParsingError = false;
/// Whether to enable the extensions drift makes to the sql grammar.
final bool enableDriftExtensions;
@ -242,31 +239,8 @@ class Parser {
final first = _peek;
final foundComponents = <PartOfDriftFile?>[];
// (we try again if the last statement had a parsing error)
// first, parse import statements
for (var stmt = _parseAsStatement(_import);
stmt != null || _lastStmtHadParsingError;
stmt = _parseAsStatement(_import)) {
foundComponents.add(stmt);
}
// next, table declarations
for (var stmt = _parseAsStatement(_create);
stmt != null || _lastStmtHadParsingError;
stmt = _parseAsStatement(_create)) {
foundComponents.add(stmt);
}
// finally, declared statements
for (var stmt = _parseAsStatement(_declaredStatement);
stmt != null || _lastStmtHadParsingError;
stmt = _parseAsStatement(_declaredStatement)) {
foundComponents.add(stmt);
}
if (!_isAtEnd) {
_error('Expected the file to end here.');
while (!_isAtEnd) {
foundComponents.add(_parseAsStatement(_partOfDriftFile));
}
foundComponents.removeWhere((c) => c == null);
@ -275,11 +249,26 @@ class Parser {
if (foundComponents.isNotEmpty) {
file.setSpan(first, _previous);
} else {
_suggestHintForTokens([TokenType.create, TokenType.import]);
if (_reportAutoComplete) {}
file.setSpan(first, first); // empty file
}
return file;
}
PartOfDriftFile _partOfDriftFile() {
final found = _import() ?? _create() ?? _declaredStatement();
if (found != null) {
return found;
}
_error('Expected `IMPORT`, `CREATE`, or an identifier starting a compiled '
'query.');
}
ImportStatement? _import() {
if (_matchOne(TokenType.import)) {
final importToken = _previous;
@ -393,7 +382,6 @@ class Parser {
/// semicolon if one exists.
T? _parseAsStatement<T extends Statement>(T? Function() parser,
{bool requireSemicolon = true}) {
_lastStmtHadParsingError = false;
final first = _peek;
T? result;
try {
@ -405,7 +393,6 @@ class Parser {
result.setSpan(first, _previous);
}
} on ParsingError {
_lastStmtHadParsingError = true;
// the error is added to the list errors, so ignore. We skip after the
// next semicolon to parse the next statement.
_synchronize(TokenType.semicolon, skipTarget: true);
@ -422,13 +409,15 @@ class Parser {
return result;
}
List<CrudStatement> _crudStatements() {
List<CrudStatement> _crudStatements(bool Function() reachedEnd) {
final stmts = <CrudStatement>[];
for (var stmt = _parseAsStatement(_crud);
stmt != null || _lastStmtHadParsingError;
stmt = _parseAsStatement(_crud)) {
if (stmt != null) stmts.add(stmt);
while (!reachedEnd()) {
final stmt = _parseAsStatement(_crud);
if (stmt != null) {
stmts.add(stmt);
}
}
return stmts;
@ -437,7 +426,7 @@ class Parser {
/// Parses a block, which consists of statements between `BEGIN` and `END`.
Block _consumeBlock() {
final begin = _consume(TokenType.begin, 'Expected BEGIN');
final stmts = _crudStatements();
final stmts = _crudStatements(() => _check(TokenType.end));
final end = _consume(TokenType.end, 'Expected END');
return Block(stmts)
@ -449,7 +438,8 @@ class Parser {
TransactionBlock _transactionBlock() {
final first = _peek;
final begin = _beginStatement();
final stmts = _crudStatements();
final stmts = _crudStatements(
() => _checkAny(const [TokenType.commit, TokenType.end]));
final end = _commit();
return TransactionBlock(begin: begin, innerStatements: stmts, commit: end)
@ -889,6 +879,7 @@ class Parser {
}
return FunctionExpression(name: first.identifier, parameters: parameters)
..nameToken = first
..setSpan(first, rightParen);
} else {
// Ok, just a regular reference then

View File

@ -219,4 +219,21 @@ SELECT DISTINCT A.* FROM works A, works B ON A.id =
]),
);
});
test('allows statements to appear in any order', () {
final result =
SqlEngine(EngineOptions(useDriftExtensions: true)).parseDriftFile('''
CREATE TABLE foo (
a INTEGER NOT NULL
);
import 'b.dart';
a: SELECT * FROM foo;
CREATE INDEX x ON foo (a);
''');
expect(result.errors, isEmpty);
});
}