mirror of https://github.com/AMT-Cheif/drift.git
450 lines
12 KiB
Dart
450 lines
12 KiB
Dart
import 'package:analyzer/dart/analysis/features.dart';
|
|
import 'package:analyzer/dart/analysis/utilities.dart';
|
|
import 'package:analyzer/dart/ast/ast.dart';
|
|
import 'package:analyzer/file_system/memory_file_system.dart';
|
|
import 'package:build/build.dart';
|
|
import 'package:build_test/build_test.dart';
|
|
import 'package:collection/collection.dart';
|
|
import 'package:test/test.dart';
|
|
|
|
import '../utils.dart';
|
|
|
|
void main() {
|
|
test(
|
|
'generates const constructor for data classes can companion classes',
|
|
() async {
|
|
final writer = await emulateDriftBuild(
|
|
inputs: const {
|
|
'a|lib/main.dart': r'''
|
|
import 'package:drift/drift.dart';
|
|
|
|
part 'main.drift.dart';
|
|
|
|
class Users extends Table {
|
|
IntColumn get id => integer().autoIncrement()();
|
|
TextColumn get name => text()();
|
|
}
|
|
|
|
@DriftDatabase(
|
|
tables: [Users],
|
|
)
|
|
class Database extends _$Database {}
|
|
'''
|
|
},
|
|
);
|
|
|
|
checkOutputs(const {
|
|
'a|lib/main.drift.dart': _GeneratesConstDataClasses(
|
|
{'User', 'UsersCompanion'},
|
|
),
|
|
}, writer.dartOutputs, writer.writer);
|
|
},
|
|
tags: 'analyzer',
|
|
);
|
|
|
|
test(
|
|
'generates async mapping code for existing row class with async factory',
|
|
() async {
|
|
final writer = await emulateDriftBuild(
|
|
inputs: const {
|
|
'a|lib/main.dart': r'''
|
|
import 'package:drift/drift.dart';
|
|
|
|
part 'main.drift.dart';
|
|
|
|
@UseRowClass(MyCustomClass, constructor: 'load')
|
|
class Tbl extends Table {
|
|
TextColumn get foo => text()();
|
|
IntColumn get bar => integer()();
|
|
}
|
|
|
|
class MyCustomClass {
|
|
static Future<MyCustomClass> load(String foo, int bar) async {
|
|
throw 'stub';
|
|
}
|
|
}
|
|
|
|
@DriftDatabase(
|
|
tables: [Tbl],
|
|
)
|
|
class Database extends _$Database {}
|
|
'''
|
|
},
|
|
);
|
|
|
|
checkOutputs({
|
|
'a|lib/main.drift.dart': decodedMatches(contains(r'''
|
|
@override
|
|
Future<MyCustomClass> map(Map<String, dynamic> data,
|
|
{String? tablePrefix}) async {
|
|
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
|
return await MyCustomClass.load(
|
|
attachedDatabase.typeMapping
|
|
.read(DriftSqlType.string, data['${effectivePrefix}foo'])!,
|
|
attachedDatabase.typeMapping
|
|
.read(DriftSqlType.int, data['${effectivePrefix}bar'])!,
|
|
);
|
|
}
|
|
''')),
|
|
}, writer.dartOutputs, writer.writer);
|
|
},
|
|
tags: 'analyzer',
|
|
);
|
|
test(
|
|
'It should generate a type converter using the EnumNameConverter when textEnum is used',
|
|
() async {
|
|
final writer = await emulateDriftBuild(
|
|
inputs: const {
|
|
'a|lib/main.dart': r'''
|
|
import 'package:drift/drift.dart';
|
|
|
|
part 'main.drift.dart';
|
|
|
|
enum Priority { low, medium, high }
|
|
|
|
class Todos extends Table {
|
|
IntColumn get id => integer().autoIncrement()();
|
|
TextColumn get priority => textEnum<Priority>()();
|
|
}
|
|
|
|
@DriftDatabase(
|
|
tables: [Todos],
|
|
)
|
|
class Database extends _$Database {}
|
|
'''
|
|
},
|
|
);
|
|
|
|
checkOutputs({
|
|
'a|lib/main.drift.dart': decodedMatches(contains(r'''
|
|
static JsonTypeConverter2<Priority, String, String> $converterpriority =
|
|
const EnumNameConverter<Priority>(Priority.values);''')),
|
|
}, writer.dartOutputs, writer.writer);
|
|
},
|
|
tags: 'analyzer',
|
|
);
|
|
|
|
test('can write mixins providing toColumns method', () async {
|
|
final writer = await emulateDriftBuild(
|
|
inputs: {
|
|
'a|lib/main.drift': '''
|
|
import 'row.dart';
|
|
|
|
CREATE TABLE users (
|
|
id INTEGER NOT NULL PRIMARY KEY,
|
|
name TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE posts (
|
|
id INTEGER NOT NULL PRIMARY KEY,
|
|
content TEXT NOT NULL
|
|
) WITH Post;
|
|
''',
|
|
'a|lib/row.dart': '''
|
|
import 'main.drift.dart';
|
|
|
|
class Post with PostsToColumns {
|
|
@override
|
|
final int id;
|
|
@override
|
|
final String content;
|
|
|
|
Post(this.id, this.content);
|
|
}
|
|
''',
|
|
},
|
|
logger: loggerThat(neverEmits(anything)),
|
|
modularBuild: true,
|
|
options: BuilderOptions({'write_to_columns_mixins': true}),
|
|
);
|
|
|
|
checkOutputs({
|
|
'a|lib/main.drift.dart': decodedMatches(contains('''
|
|
mixin PostsToColumns implements i1.Insertable<i2.Post> {
|
|
int get id;
|
|
String get content;
|
|
@override
|
|
Map<String, i1.Expression> toColumns(bool nullToAbsent) {
|
|
final map = <String, i1.Expression>{};
|
|
map['id'] = i1.Variable<int>(id);
|
|
map['content'] = i1.Variable<String>(content);
|
|
return map;
|
|
}
|
|
}
|
|
''')),
|
|
}, writer.dartOutputs, writer.writer);
|
|
});
|
|
|
|
test('generates correct fromJson for nullable converters', () async {
|
|
// Regression test for https://github.com/simolus3/drift/issues/2281
|
|
final result = await emulateDriftBuild(
|
|
inputs: {
|
|
'a|lib/a.dart': '''
|
|
import 'package:drift/drift.dart';
|
|
|
|
class MyModel {}
|
|
|
|
class MyConverter
|
|
extends TypeConverter<MyModel?, String?>
|
|
with JsonTypeConverter<MyModel?, String?> {
|
|
const MyConverter();
|
|
}
|
|
|
|
class MyTable extends Table {
|
|
TextColumn get invoiceContact => text()
|
|
.map(const MyConverter()).nullable()();
|
|
}
|
|
''',
|
|
},
|
|
modularBuild: true,
|
|
);
|
|
|
|
checkOutputs({
|
|
'a|lib/a.drift.dart': decodedMatches(contains(r'''
|
|
factory MyTableData.fromJson(Map<String, dynamic> json,
|
|
{i0.ValueSerializer? serializer}) {
|
|
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
|
return MyTableData(
|
|
invoiceContact: i1.$MyTableTable.$converterinvoiceContact
|
|
.fromJson(serializer.fromJson<String?>(json['invoiceContact'])),
|
|
);
|
|
}''')),
|
|
}, result.dartOutputs, result.writer);
|
|
});
|
|
|
|
test('generates correct companions for modular row classes', () async {
|
|
final result = await emulateDriftBuild(
|
|
inputs: {
|
|
'a|lib/a.dart': '''
|
|
import 'package:drift/drift.dart';
|
|
|
|
@UseRowClass(Item, generateInsertable: true)
|
|
class ItemTable extends Table {
|
|
IntColumn get id => integer().autoIncrement()();
|
|
}
|
|
|
|
class Item {
|
|
final int id;
|
|
Item(this.id);
|
|
}
|
|
''',
|
|
},
|
|
modularBuild: true,
|
|
);
|
|
|
|
checkOutputs(
|
|
{
|
|
'a|lib/a.drift.dart': decodedMatches(
|
|
allOf(
|
|
// The toString() definition for companions was broken and included
|
|
// the import prefix of the companion.
|
|
contains("StringBuffer('ItemTableCompanion(')"),
|
|
|
|
// The extension should also reference the row class correctly
|
|
contains(r'''
|
|
class _$ItemInsertable implements i0.Insertable<i1.Item> {
|
|
i1.Item _object;
|
|
_$ItemInsertable(this._object);
|
|
@override
|
|
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
|
return i2.ItemTableCompanion(
|
|
id: i0.Value(_object.id),
|
|
).toColumns(false);
|
|
}
|
|
}
|
|
|
|
extension ItemToInsertable on i1.Item {
|
|
_$ItemInsertable toInsertable() {
|
|
return _$ItemInsertable(this);
|
|
}
|
|
}
|
|
'''),
|
|
),
|
|
),
|
|
},
|
|
result.dartOutputs,
|
|
result.writer,
|
|
);
|
|
});
|
|
|
|
test(
|
|
'generates fromJson and toJson with the sql column names as json keys',
|
|
() async {
|
|
final writer = await emulateDriftBuild(
|
|
options: const BuilderOptions({
|
|
'use_sql_column_name_as_json_key': true,
|
|
}),
|
|
inputs: const {
|
|
'a|lib/main.dart': r'''
|
|
import 'package:drift/drift.dart';
|
|
|
|
part 'main.drift.dart';
|
|
|
|
class MyTable extends Table {
|
|
TextColumn get myFirstColumn => text()();
|
|
IntColumn get mySecondColumn => integer()();
|
|
}
|
|
|
|
|
|
@DriftDatabase(
|
|
tables: [MyTable],
|
|
)
|
|
class Database extends _$Database {}
|
|
'''
|
|
},
|
|
);
|
|
|
|
checkOutputs({
|
|
'a|lib/main.drift.dart': decodedMatches(contains(r'''
|
|
factory MyTableData.fromJson(Map<String, dynamic> json,
|
|
{ValueSerializer? serializer}) {
|
|
serializer ??= driftRuntimeOptions.defaultSerializer;
|
|
return MyTableData(
|
|
myFirstColumn: serializer.fromJson<String>(json['my_first_column']),
|
|
mySecondColumn: serializer.fromJson<int>(json['my_second_column']),
|
|
);
|
|
}
|
|
@override
|
|
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
|
serializer ??= driftRuntimeOptions.defaultSerializer;
|
|
return <String, dynamic>{
|
|
'my_first_column': serializer.toJson<String>(myFirstColumn),
|
|
'my_second_column': serializer.toJson<int>(mySecondColumn),
|
|
};
|
|
}
|
|
|
|
''')),
|
|
}, writer.dartOutputs, writer.writer);
|
|
},
|
|
tags: 'analyzer',
|
|
);
|
|
|
|
test(
|
|
'It should use the provided names for the data classes and the companion class',
|
|
() async {
|
|
final writer = await emulateDriftBuild(
|
|
inputs: const {
|
|
'a|lib/main.dart': r'''
|
|
import 'package:drift/drift.dart';
|
|
|
|
part 'main.drift.dart';
|
|
|
|
@DataClassName('FirstDataClass', companion: 'FirstCompanionClass')
|
|
class FirstTable extends Table {
|
|
TextColumn get foo => text()();
|
|
IntColumn get bar => integer()();
|
|
}
|
|
|
|
@DataClassName.custom(name: 'SecondDataClass', companion: 'SecondCompanionClass')
|
|
class SecondTable extends Table {
|
|
TextColumn get foo => text()();
|
|
IntColumn get bar => integer()();
|
|
}
|
|
|
|
@DataClassName.custom(companion: 'ThirdCompanionClass')
|
|
class ThirdTable extends Table {
|
|
TextColumn get foo => text()();
|
|
IntColumn get bar => integer()();
|
|
}
|
|
|
|
@DriftDatabase(
|
|
tables: [FirstTable, SecondTable, ThirdTable],
|
|
)
|
|
class Database extends _$Database {}
|
|
'''
|
|
},
|
|
);
|
|
|
|
checkOutputs({
|
|
'a|lib/main.drift.dart': decodedMatches(allOf([
|
|
contains(
|
|
'class FirstDataClass extends DataClass implements Insertable<FirstDataClass> {',
|
|
),
|
|
contains(
|
|
'class FirstTableCompanion extends UpdateCompanion<FirstDataClass> {',
|
|
),
|
|
contains(
|
|
'class SecondDataClass extends DataClass implements Insertable<SecondDataClass> {',
|
|
),
|
|
contains(
|
|
'class SecondTableCompanion extends UpdateCompanion<SecondDataClass> {',
|
|
),
|
|
contains(
|
|
'class ThirdTableData extends DataClass implements Insertable<ThirdTableData> {',
|
|
),
|
|
contains(
|
|
'class ThirdTableCompanion extends UpdateCompanion<ThirdTableData> {',
|
|
),
|
|
])),
|
|
}, writer.dartOutputs, writer.writer);
|
|
},
|
|
tags: 'analyzer',
|
|
);
|
|
}
|
|
|
|
class _GeneratesConstDataClasses extends Matcher {
|
|
final Set<String> expectedWithConstConstructor;
|
|
|
|
const _GeneratesConstDataClasses(this.expectedWithConstConstructor);
|
|
|
|
@override
|
|
Description describe(Description description) {
|
|
return description.add('generates classes $expectedWithConstConstructor '
|
|
'const constructor.');
|
|
}
|
|
|
|
@override
|
|
bool matches(dynamic desc, Map matchState) {
|
|
// Parse the file, assure we don't have final fields in data classes.
|
|
final resourceProvider = MemoryResourceProvider();
|
|
if (desc is List<int>) {
|
|
resourceProvider.newFileWithBytes('/foo.dart', desc);
|
|
} else if (desc is String) {
|
|
resourceProvider.newFile('/foo.dart', desc);
|
|
} else {
|
|
desc['desc'] = 'Neither a List<int> or String - cannot be parsed';
|
|
return false;
|
|
}
|
|
|
|
final parsed = parseFile(
|
|
path: '/foo.dart',
|
|
featureSet: FeatureSet.latestLanguageVersion(),
|
|
resourceProvider: resourceProvider,
|
|
throwIfDiagnostics: true,
|
|
).unit;
|
|
|
|
final remaining = expectedWithConstConstructor.toSet();
|
|
|
|
final definedClasses = parsed.declarations.whereType<ClassDeclaration>();
|
|
for (final definedClass in definedClasses) {
|
|
if (expectedWithConstConstructor.contains(definedClass.name.lexeme)) {
|
|
final constructor = definedClass.members
|
|
.whereType<ConstructorDeclaration>()
|
|
.firstWhereOrNull((e) => e.name == null);
|
|
if (constructor?.constKeyword == null) {
|
|
matchState['desc'] = 'Constructor ${definedClass.name.lexeme} is not '
|
|
'const.';
|
|
return false;
|
|
}
|
|
|
|
remaining.remove(definedClass.name.lexeme);
|
|
}
|
|
}
|
|
|
|
// Also ensure that all expected classes were generated.
|
|
if (remaining.isNotEmpty) {
|
|
matchState['desc'] = 'Did not generate $remaining classes';
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
@override
|
|
Description describeMismatch(dynamic item, Description mismatchDescription,
|
|
Map matchState, bool verbose) {
|
|
return mismatchDescription
|
|
.add((matchState['desc'] as String?) ?? 'Had syntax errors');
|
|
}
|
|
}
|