diff --git a/moor/lib/src/dsl/database.dart b/moor/lib/src/dsl/database.dart index 160df839..c364210c 100644 --- a/moor/lib/src/dsl/database.dart +++ b/moor/lib/src/dsl/database.dart @@ -42,15 +42,22 @@ class UseMoor { /// Moor will generate two methods for you: `userById(int id)` and /// `watchUserById(int id)`. /// {@endtemplate} - @experimental final Map queries; + /// Defines the `.moor` files to include when building the table structure for + /// this database. + /// + /// Please note that this feature is experimental at the moment. + @experimental + final Set include; + /// Use this class as an annotation to inform moor_generator that a database /// class should be generated using the specified [UseMoor.tables]. const UseMoor({ @required this.tables, this.daos = const [], - @experimental this.queries = const {}, + this.queries = const {}, + @experimental this.include = const {}, }); } diff --git a/moor/test/data/tables/test.moor b/moor/test/data/tables/test.moor new file mode 100644 index 00000000..1146d167 --- /dev/null +++ b/moor/test/data/tables/test.moor @@ -0,0 +1,10 @@ +CREATE TABLE state ( + id INT NOT NULL PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL +); + +CREATE TABLE experiments ( + id INT NOT NULL PRIMARY KEY AUTOINCREMENT, + description TEXT NOT NULL, + state INT REFERENCES state(id) ON UPDATE CASCADE ON DELETE SET NULL +) \ No newline at end of file diff --git a/moor/test/data/tables/todos.dart b/moor/test/data/tables/todos.dart index 9a6d1543..1fcca815 100644 --- a/moor/test/data/tables/todos.dart +++ b/moor/test/data/tables/todos.dart @@ -88,6 +88,7 @@ class CustomConverter extends TypeConverter { PureDefaults, ], daos: [SomeDao], + include: {'test.moor'}, queries: { 'allTodosWithCategory': 'SELECT t.*, c.id as catId, c."desc" as catDesc ' 'FROM todos t INNER JOIN categories c ON c.id = t.category', diff --git a/moor/test/data/tables/todos.g.dart b/moor/test/data/tables/todos.g.dart index 3982fe20..32a08890 100644 --- a/moor/test/data/tables/todos.g.dart +++ b/moor/test/data/tables/todos.g.dart @@ -1187,6 +1187,331 @@ class $PureDefaultsTable extends PureDefaults } } +class StateData extends DataClass implements Insertable { + final int id; + final String name; + StateData({@required this.id, @required this.name}); + factory StateData.fromData(Map data, GeneratedDatabase db, + {String prefix}) { + final effectivePrefix = prefix ?? ''; + final intType = db.typeSystem.forDartType(); + final stringType = db.typeSystem.forDartType(); + return StateData( + id: intType.mapFromDatabaseResponse(data['${effectivePrefix}id']), + name: stringType.mapFromDatabaseResponse(data['${effectivePrefix}name']), + ); + } + factory StateData.fromJson(Map json, + {ValueSerializer serializer = const ValueSerializer.defaults()}) { + return StateData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + ); + } + @override + Map toJson( + {ValueSerializer serializer = const ValueSerializer.defaults()}) { + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + }; + } + + @override + T createCompanion>(bool nullToAbsent) { + return StateCompanion( + id: id == null && nullToAbsent ? const Value.absent() : Value(id), + name: name == null && nullToAbsent ? const Value.absent() : Value(name), + ) as T; + } + + StateData copyWith({int id, String name}) => StateData( + id: id ?? this.id, + name: name ?? this.name, + ); + @override + String toString() { + return (StringBuffer('StateData(') + ..write('id: $id, ') + ..write('name: $name') + ..write(')')) + .toString(); + } + + @override + int get hashCode => $mrjf($mrjc(id.hashCode, name.hashCode)); + @override + bool operator ==(other) => + identical(this, other) || + (other is StateData && other.id == id && other.name == name); +} + +class StateCompanion extends UpdateCompanion { + final Value id; + final Value name; + const StateCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + }); +} + +class $StateTable extends Table with TableInfo<$StateTable, StateData> { + final GeneratedDatabase _db; + final String _alias; + $StateTable(this._db, [this._alias]); + final VerificationMeta _idMeta = const VerificationMeta('id'); + GeneratedIntColumn _id; + @override + GeneratedIntColumn get id => _id ??= _constructId(); + GeneratedIntColumn _constructId() { + return GeneratedIntColumn('id', $tableName, false, + hasAutoIncrement: true, + $customConstraints: 'NOT NULL PRIMARY KEY AUTOINCREMENT'); + } + + final VerificationMeta _nameMeta = const VerificationMeta('name'); + GeneratedTextColumn _name; + @override + GeneratedTextColumn get name => _name ??= _constructName(); + GeneratedTextColumn _constructName() { + return GeneratedTextColumn('name', $tableName, false, + $customConstraints: 'NOT NULL'); + } + + @override + List get $columns => [id, name]; + @override + $StateTable get asDslTable => this; + @override + String get $tableName => _alias ?? 'state'; + @override + final String actualTableName = 'state'; + @override + VerificationContext validateIntegrity(StateCompanion d, + {bool isInserting = false}) { + final context = VerificationContext(); + if (d.id.present) { + context.handle(_idMeta, id.isAcceptableValue(d.id.value, _idMeta)); + } else if (id.isRequired && isInserting) { + context.missing(_idMeta); + } + if (d.name.present) { + context.handle( + _nameMeta, name.isAcceptableValue(d.name.value, _nameMeta)); + } else if (name.isRequired && isInserting) { + context.missing(_nameMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + StateData map(Map data, {String tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : null; + return StateData.fromData(data, _db, prefix: effectivePrefix); + } + + @override + Map entityToSql(StateCompanion d) { + final map = {}; + if (d.id.present) { + map['id'] = Variable(d.id.value); + } + if (d.name.present) { + map['name'] = Variable(d.name.value); + } + return map; + } + + @override + $StateTable createAlias(String alias) { + return $StateTable(_db, alias); + } +} + +class Experiment extends DataClass implements Insertable { + final int id; + final String description; + final int state; + Experiment({@required this.id, @required this.description, this.state}); + factory Experiment.fromData(Map data, GeneratedDatabase db, + {String prefix}) { + final effectivePrefix = prefix ?? ''; + final intType = db.typeSystem.forDartType(); + final stringType = db.typeSystem.forDartType(); + return Experiment( + id: intType.mapFromDatabaseResponse(data['${effectivePrefix}id']), + description: stringType + .mapFromDatabaseResponse(data['${effectivePrefix}description']), + state: intType.mapFromDatabaseResponse(data['${effectivePrefix}state']), + ); + } + factory Experiment.fromJson(Map json, + {ValueSerializer serializer = const ValueSerializer.defaults()}) { + return Experiment( + id: serializer.fromJson(json['id']), + description: serializer.fromJson(json['description']), + state: serializer.fromJson(json['state']), + ); + } + @override + Map toJson( + {ValueSerializer serializer = const ValueSerializer.defaults()}) { + return { + 'id': serializer.toJson(id), + 'description': serializer.toJson(description), + 'state': serializer.toJson(state), + }; + } + + @override + T createCompanion>(bool nullToAbsent) { + return ExperimentsCompanion( + id: id == null && nullToAbsent ? const Value.absent() : Value(id), + description: description == null && nullToAbsent + ? const Value.absent() + : Value(description), + state: + state == null && nullToAbsent ? const Value.absent() : Value(state), + ) as T; + } + + Experiment copyWith({int id, String description, int state}) => Experiment( + id: id ?? this.id, + description: description ?? this.description, + state: state ?? this.state, + ); + @override + String toString() { + return (StringBuffer('Experiment(') + ..write('id: $id, ') + ..write('description: $description, ') + ..write('state: $state') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + $mrjf($mrjc(id.hashCode, $mrjc(description.hashCode, state.hashCode))); + @override + bool operator ==(other) => + identical(this, other) || + (other is Experiment && + other.id == id && + other.description == description && + other.state == state); +} + +class ExperimentsCompanion extends UpdateCompanion { + final Value id; + final Value description; + final Value state; + const ExperimentsCompanion({ + this.id = const Value.absent(), + this.description = const Value.absent(), + this.state = const Value.absent(), + }); +} + +class $ExperimentsTable extends Table + with TableInfo<$ExperimentsTable, Experiment> { + final GeneratedDatabase _db; + final String _alias; + $ExperimentsTable(this._db, [this._alias]); + final VerificationMeta _idMeta = const VerificationMeta('id'); + GeneratedIntColumn _id; + @override + GeneratedIntColumn get id => _id ??= _constructId(); + GeneratedIntColumn _constructId() { + return GeneratedIntColumn('id', $tableName, false, + hasAutoIncrement: true, + $customConstraints: 'NOT NULL PRIMARY KEY AUTOINCREMENT'); + } + + final VerificationMeta _descriptionMeta = + const VerificationMeta('description'); + GeneratedTextColumn _description; + @override + GeneratedTextColumn get description => + _description ??= _constructDescription(); + GeneratedTextColumn _constructDescription() { + return GeneratedTextColumn('description', $tableName, false, + $customConstraints: 'NOT NULL'); + } + + final VerificationMeta _stateMeta = const VerificationMeta('state'); + GeneratedIntColumn _state; + @override + GeneratedIntColumn get state => _state ??= _constructState(); + GeneratedIntColumn _constructState() { + return GeneratedIntColumn('state', $tableName, true, + $customConstraints: + 'REFERENCES state(id) ON UPDATE CASCADE ON DELETE SET NULL'); + } + + @override + List get $columns => [id, description, state]; + @override + $ExperimentsTable get asDslTable => this; + @override + String get $tableName => _alias ?? 'experiments'; + @override + final String actualTableName = 'experiments'; + @override + VerificationContext validateIntegrity(ExperimentsCompanion d, + {bool isInserting = false}) { + final context = VerificationContext(); + if (d.id.present) { + context.handle(_idMeta, id.isAcceptableValue(d.id.value, _idMeta)); + } else if (id.isRequired && isInserting) { + context.missing(_idMeta); + } + if (d.description.present) { + context.handle(_descriptionMeta, + description.isAcceptableValue(d.description.value, _descriptionMeta)); + } else if (description.isRequired && isInserting) { + context.missing(_descriptionMeta); + } + if (d.state.present) { + context.handle( + _stateMeta, state.isAcceptableValue(d.state.value, _stateMeta)); + } else if (state.isRequired && isInserting) { + context.missing(_stateMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + Experiment map(Map data, {String tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : null; + return Experiment.fromData(data, _db, prefix: effectivePrefix); + } + + @override + Map entityToSql(ExperimentsCompanion d) { + final map = {}; + if (d.id.present) { + map['id'] = Variable(d.id.value); + } + if (d.description.present) { + map['description'] = Variable(d.description.value); + } + if (d.state.present) { + map['state'] = Variable(d.state.value); + } + return map; + } + + @override + $ExperimentsTable createAlias(String alias) { + return $ExperimentsTable(_db, alias); + } +} + class AllTodosWithCategoryResult { final int id; final String title; @@ -1229,6 +1554,10 @@ abstract class _$TodoDb extends GeneratedDatabase { $PureDefaultsTable _pureDefaults; $PureDefaultsTable get pureDefaults => _pureDefaults ??= $PureDefaultsTable(this); + $StateTable _state; + $StateTable get state => _state ??= $StateTable(this); + $ExperimentsTable _experiments; + $ExperimentsTable get experiments => _experiments ??= $ExperimentsTable(this); SomeDao _someDao; SomeDao get someDao => _someDao ??= SomeDao(this as TodoDb); AllTodosWithCategoryResult _rowToAllTodosWithCategoryResult(QueryRow row) { @@ -1367,7 +1696,9 @@ abstract class _$TodoDb extends GeneratedDatabase { users, sharedTodos, tableWithoutPK, - pureDefaults + pureDefaults, + state, + experiments ]; } diff --git a/moor_generator/lib/src/parser/use_moor_parser.dart b/moor_generator/lib/src/parser/use_moor_parser.dart index bc4f59b6..999c1f2e 100644 --- a/moor_generator/lib/src/parser/use_moor_parser.dart +++ b/moor_generator/lib/src/parser/use_moor_parser.dart @@ -17,8 +17,16 @@ class UseMoorParser { final tableTypes = annotation.peek('tables').listValue.map((obj) => obj.toTypeValue()); final queryStrings = annotation.peek('queries')?.mapValue ?? {}; + final includes = annotation + .read('include') + .objectValue + .toSetValue() + ?.map((e) => e.toStringValue()) ?? + {}; final parsedTables = await session.parseTables(tableTypes, element); + parsedTables.addAll(await session.resolveIncludes(includes)); + final parsedQueries = await session.parseQueries(queryStrings, parsedTables); final daoTypes = _readDaoTypes(annotation); diff --git a/moor_generator/lib/src/state/session.dart b/moor_generator/lib/src/state/session.dart index 8dc717c8..6bb65486 100644 --- a/moor_generator/lib/src/state/session.dart +++ b/moor_generator/lib/src/state/session.dart @@ -10,7 +10,9 @@ import 'package:moor_generator/src/model/specified_database.dart'; import 'package:moor_generator/src/model/specified_table.dart'; import 'package:moor_generator/src/model/sql_query.dart'; import 'package:moor_generator/src/parser/column_parser.dart'; +import 'package:moor_generator/src/parser/moor/moor_analyzer.dart'; import 'package:moor_generator/src/parser/sql/sql_parser.dart'; +import 'package:moor_generator/src/parser/sql/type_mapping.dart'; import 'package:moor_generator/src/parser/table_parser.dart'; import 'package:moor_generator/src/parser/use_dao_parser.dart'; import 'package:moor_generator/src/parser/use_moor_parser.dart'; @@ -78,7 +80,34 @@ class GeneratorSession { } else { return _tableParser.parse(type.element as ClassElement); } - })); + })).then((list) => List.from(list)); // make growable + } + + Future> resolveIncludes(Iterable paths) async { + final mapper = TypeMapper(); + final foundTables = []; + + for (var path in paths) { + final asset = AssetId.resolve(path, from: step.inputId); + String content; + try { + content = await step.readAsString(asset); + } catch (e) { + errors.add(MoorError( + critical: true, + message: 'The included file $path could not be found')); + } + + final parsed = await MoorAnalyzer(content).analyze(); + foundTables.addAll( + parsed.parsedFile.declaredTables.map((t) => t.extractTable(mapper))); + + for (var parseError in parsed.errors) { + errors.add(MoorError(message: "Can't parse sql in $path: $parseError")); + } + } + + return foundTables; } /// Parses a column from a getter [e] declared inside a table class and its diff --git a/moor_generator/lib/src/writer/table_writer.dart b/moor_generator/lib/src/writer/table_writer.dart index 2ca62b27..ab723bf4 100644 --- a/moor_generator/lib/src/writer/table_writer.dart +++ b/moor_generator/lib/src/writer/table_writer.dart @@ -24,7 +24,7 @@ class TableWriter { void writeTableInfoClass(StringBuffer buffer) { final dataClass = table.dartTypeName; - final tableDslName = table.fromClass?.name ?? 'dynamic'; + final tableDslName = table.fromClass?.name ?? 'Table'; // class UsersTable extends Users implements TableInfo { buffer