From 2e7c079e4db1ee377304fec62127a291b49cfd24 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Wed, 6 Mar 2019 21:43:16 +0100 Subject: [PATCH] Custom primary keys --- sally/example/example.dart | 21 +- sally/example/example.g.dart | 349 ++++++++++++++++++ sally/lib/src/dsl/columns.dart | 4 - sally/lib/src/dsl/table.dart | 19 +- sally/lib/src/runtime/migration.dart | 23 ++ sally/test/data/tables/todos.dart | 10 +- sally/test/data/tables/todos.g.dart | 88 ++++- sally/test/schema_test.dart | 5 +- sally_flutter/README.md | 2 +- sally_flutter/lib/sally_flutter.dart | 12 + .../lib/src/model/specified_table.dart | 6 +- .../lib/src/parser/table_parser.dart | 43 ++- sally_generator/lib/src/sally_generator.dart | 23 +- .../lib/src/writer/table_writer.dart | 42 ++- sally_generator/test/parser/parser_test.dart | 4 +- 15 files changed, 596 insertions(+), 55 deletions(-) create mode 100644 sally/example/example.g.dart diff --git a/sally/example/example.dart b/sally/example/example.dart index afc797d6..4881613a 100644 --- a/sally/example/example.dart +++ b/sally/example/example.dart @@ -1,5 +1,7 @@ import 'package:sally/sally.dart'; +part 'example.g.dart'; + // Define tables that can model a database of recipes. @DataClassName('Category') @@ -22,7 +24,6 @@ class Ingredients extends Table { } class IngredientInRecipes extends Table { - @override String get tableName => 'recipe_ingredients'; @@ -30,8 +31,22 @@ class IngredientInRecipes extends Table { @override Set get primaryKey => {recipe, ingredient}; - IntColumn get recipe => integer().autoIncrement()(); - IntColumn get ingredient => integer().autoIncrement()(); + IntColumn get recipe => integer()(); + IntColumn get ingredient => integer()(); IntColumn get amountInGrams => integer().named('amount')(); } + +@UseSally(tables: [Categories, Recipes, Ingredients, IngredientInRecipes]) +class Database extends _$Database { + Database(QueryExecutor e) : super(e); + + @override + int get schemaVersion => 1; + + @override + MigrationStrategy get migration => MigrationStrategy(onFinished: () async { + // populate data + await into(categories).insert(Category(description: 'Sweets')); + }); +} diff --git a/sally/example/example.g.dart b/sally/example/example.g.dart new file mode 100644 index 00000000..bf387497 --- /dev/null +++ b/sally/example/example.g.dart @@ -0,0 +1,349 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'example.dart'; + +// ************************************************************************** +// SallyGenerator +// ************************************************************************** + +class Category { + final int id; + final String description; + Category({this.id, this.description}); + factory Category.fromData(Map data, GeneratedDatabase db) { + final intType = db.typeSystem.forDartType(); + final stringType = db.typeSystem.forDartType(); + return Category( + id: intType.mapFromDatabaseResponse(data['id']), + description: stringType.mapFromDatabaseResponse(data['description']), + ); + } + Category copyWith({int id, String description}) => Category( + id: id ?? this.id, + description: description ?? this.description, + ); + @override + int get hashCode => (id.hashCode) * 31 + description.hashCode; + @override + bool operator ==(other) => + identical(this, other) || + (other is Category && other.id == id && other.description == description); +} + +class $CategoriesTable extends Categories + implements TableInfo { + final GeneratedDatabase _db; + $CategoriesTable(this._db); + @override + GeneratedIntColumn get id => + GeneratedIntColumn('id', false, hasAutoIncrement: true); + @override + GeneratedTextColumn get description => GeneratedTextColumn( + 'description', + true, + ); + @override + List get $columns => [id, description]; + @override + Categories get asDslTable => this; + @override + String get $tableName => 'categories'; + @override + bool validateIntegrity(Category instance, bool isInserting) => + id.isAcceptableValue(instance.id, isInserting) && + description.isAcceptableValue(instance.description, isInserting); + @override + Set get $primaryKey => null; + @override + Category map(Map data) { + return Category.fromData(data, _db); + } + + @override + Map entityToSql(Category d) { + final map = {}; + if (d.id != null) { + map['id'] = Variable(d.id); + } + if (d.description != null) { + map['description'] = Variable(d.description); + } + return map; + } +} + +class Recipe { + final int id; + final String title; + final String instructions; + final int category; + Recipe({this.id, this.title, this.instructions, this.category}); + factory Recipe.fromData(Map data, GeneratedDatabase db) { + final intType = db.typeSystem.forDartType(); + final stringType = db.typeSystem.forDartType(); + return Recipe( + id: intType.mapFromDatabaseResponse(data['id']), + title: stringType.mapFromDatabaseResponse(data['title']), + instructions: stringType.mapFromDatabaseResponse(data['instructions']), + category: intType.mapFromDatabaseResponse(data['category']), + ); + } + Recipe copyWith({int id, String title, String instructions, int category}) => + Recipe( + id: id ?? this.id, + title: title ?? this.title, + instructions: instructions ?? this.instructions, + category: category ?? this.category, + ); + @override + int get hashCode => + (((id.hashCode) * 31 + title.hashCode) * 31 + instructions.hashCode) * + 31 + + category.hashCode; + @override + bool operator ==(other) => + identical(this, other) || + (other is Recipe && + other.id == id && + other.title == title && + other.instructions == instructions && + other.category == category); +} + +class $RecipesTable extends Recipes implements TableInfo { + final GeneratedDatabase _db; + $RecipesTable(this._db); + @override + GeneratedIntColumn get id => + GeneratedIntColumn('id', false, hasAutoIncrement: true); + @override + GeneratedTextColumn get title => + GeneratedTextColumn('title', false, maxTextLength: 16); + @override + GeneratedTextColumn get instructions => GeneratedTextColumn( + 'instructions', + false, + ); + @override + GeneratedIntColumn get category => GeneratedIntColumn( + 'category', + true, + ); + @override + List get $columns => [id, title, instructions, category]; + @override + Recipes get asDslTable => this; + @override + String get $tableName => 'recipes'; + @override + bool validateIntegrity(Recipe instance, bool isInserting) => + id.isAcceptableValue(instance.id, isInserting) && + title.isAcceptableValue(instance.title, isInserting) && + instructions.isAcceptableValue(instance.instructions, isInserting) && + category.isAcceptableValue(instance.category, isInserting); + @override + Set get $primaryKey => null; + @override + Recipe map(Map data) { + return Recipe.fromData(data, _db); + } + + @override + Map entityToSql(Recipe d) { + final map = {}; + if (d.id != null) { + map['id'] = Variable(d.id); + } + if (d.title != null) { + map['title'] = Variable(d.title); + } + if (d.instructions != null) { + map['instructions'] = Variable(d.instructions); + } + if (d.category != null) { + map['category'] = Variable(d.category); + } + return map; + } +} + +class Ingredient { + final int id; + final String name; + final int caloriesPer100g; + Ingredient({this.id, this.name, this.caloriesPer100g}); + factory Ingredient.fromData(Map data, GeneratedDatabase db) { + final intType = db.typeSystem.forDartType(); + final stringType = db.typeSystem.forDartType(); + return Ingredient( + id: intType.mapFromDatabaseResponse(data['id']), + name: stringType.mapFromDatabaseResponse(data['name']), + caloriesPer100g: intType.mapFromDatabaseResponse(data['calories']), + ); + } + Ingredient copyWith({int id, String name, int caloriesPer100g}) => Ingredient( + id: id ?? this.id, + name: name ?? this.name, + caloriesPer100g: caloriesPer100g ?? this.caloriesPer100g, + ); + @override + int get hashCode => + ((id.hashCode) * 31 + name.hashCode) * 31 + caloriesPer100g.hashCode; + @override + bool operator ==(other) => + identical(this, other) || + (other is Ingredient && + other.id == id && + other.name == name && + other.caloriesPer100g == caloriesPer100g); +} + +class $IngredientsTable extends Ingredients + implements TableInfo { + final GeneratedDatabase _db; + $IngredientsTable(this._db); + @override + GeneratedIntColumn get id => + GeneratedIntColumn('id', false, hasAutoIncrement: true); + @override + GeneratedTextColumn get name => GeneratedTextColumn( + 'name', + false, + ); + @override + GeneratedIntColumn get caloriesPer100g => GeneratedIntColumn( + 'calories', + false, + ); + @override + List get $columns => [id, name, caloriesPer100g]; + @override + Ingredients get asDslTable => this; + @override + String get $tableName => 'ingredients'; + @override + bool validateIntegrity(Ingredient instance, bool isInserting) => + id.isAcceptableValue(instance.id, isInserting) && + name.isAcceptableValue(instance.name, isInserting) && + caloriesPer100g.isAcceptableValue(instance.caloriesPer100g, isInserting); + @override + Set get $primaryKey => null; + @override + Ingredient map(Map data) { + return Ingredient.fromData(data, _db); + } + + @override + Map entityToSql(Ingredient d) { + final map = {}; + if (d.id != null) { + map['id'] = Variable(d.id); + } + if (d.name != null) { + map['name'] = Variable(d.name); + } + if (d.caloriesPer100g != null) { + map['calories'] = Variable(d.caloriesPer100g); + } + return map; + } +} + +class IngredientInRecipe { + final int recipe; + final int ingredient; + final int amountInGrams; + IngredientInRecipe({this.recipe, this.ingredient, this.amountInGrams}); + factory IngredientInRecipe.fromData( + Map data, GeneratedDatabase db) { + final intType = db.typeSystem.forDartType(); + return IngredientInRecipe( + recipe: intType.mapFromDatabaseResponse(data['recipe']), + ingredient: intType.mapFromDatabaseResponse(data['ingredient']), + amountInGrams: intType.mapFromDatabaseResponse(data['amount']), + ); + } + IngredientInRecipe copyWith( + {int recipe, int ingredient, int amountInGrams}) => + IngredientInRecipe( + recipe: recipe ?? this.recipe, + ingredient: ingredient ?? this.ingredient, + amountInGrams: amountInGrams ?? this.amountInGrams, + ); + @override + int get hashCode => + ((recipe.hashCode) * 31 + ingredient.hashCode) * 31 + + amountInGrams.hashCode; + @override + bool operator ==(other) => + identical(this, other) || + (other is IngredientInRecipe && + other.recipe == recipe && + other.ingredient == ingredient && + other.amountInGrams == amountInGrams); +} + +class $IngredientInRecipesTable extends IngredientInRecipes + implements TableInfo { + final GeneratedDatabase _db; + $IngredientInRecipesTable(this._db); + @override + GeneratedIntColumn get recipe => GeneratedIntColumn( + 'recipe', + false, + ); + @override + GeneratedIntColumn get ingredient => GeneratedIntColumn( + 'ingredient', + false, + ); + @override + GeneratedIntColumn get amountInGrams => GeneratedIntColumn( + 'amount', + false, + ); + @override + List get $columns => [recipe, ingredient, amountInGrams]; + @override + IngredientInRecipes get asDslTable => this; + @override + String get $tableName => 'recipe_ingredients'; + @override + bool validateIntegrity(IngredientInRecipe instance, bool isInserting) => + recipe.isAcceptableValue(instance.recipe, isInserting) && + ingredient.isAcceptableValue(instance.ingredient, isInserting) && + amountInGrams.isAcceptableValue(instance.amountInGrams, isInserting); + @override + Set get $primaryKey => {recipe, ingredient}; + @override + IngredientInRecipe map(Map data) { + return IngredientInRecipe.fromData(data, _db); + } + + @override + Map entityToSql(IngredientInRecipe d) { + final map = {}; + if (d.recipe != null) { + map['recipe'] = Variable(d.recipe); + } + if (d.ingredient != null) { + map['ingredient'] = Variable(d.ingredient); + } + if (d.amountInGrams != null) { + map['amount'] = Variable(d.amountInGrams); + } + return map; + } +} + +abstract class _$Database extends GeneratedDatabase { + _$Database(QueryExecutor e) : super(const SqlTypeSystem.withDefaults(), e); + $CategoriesTable get categories => $CategoriesTable(this); + $RecipesTable get recipes => $RecipesTable(this); + $IngredientsTable get ingredients => $IngredientsTable(this); + $IngredientInRecipesTable get ingredientInRecipes => + $IngredientInRecipesTable(this); + @override + List get allTables => + [categories, recipes, ingredients, ingredientInRecipes]; +} diff --git a/sally/lib/src/dsl/columns.dart b/sally/lib/src/dsl/columns.dart index 0999dee7..4768b91d 100644 --- a/sally/lib/src/dsl/columns.dart +++ b/sally/lib/src/dsl/columns.dart @@ -40,10 +40,6 @@ class ColumnBuilder { /// `IntColumn get id = integer((c) => c.named('user_id'))`. Builder named(String name) => null; - @Deprecated('Ignored by the generator. Please override primaryKey in your ' - 'table class instead') - Builder primaryKey() => null; - /// Marks this column as nullable. Nullable columns should not appear in a /// primary key. Columns are non-null by default. Builder nullable() => null; diff --git a/sally/lib/src/dsl/table.dart b/sally/lib/src/dsl/table.dart index 27051ad8..a845f4eb 100644 --- a/sally/lib/src/dsl/table.dart +++ b/sally/lib/src/dsl/table.dart @@ -17,8 +17,23 @@ abstract class Table { @visibleForOverriding String get tableName => null; - /// In the future, you can override this to specify a custom primary key. This - /// is not supported by sally at the moment. + /// Override this to specify custom primary keys: + /// ```dart + /// class IngredientInRecipes extends Table { + /// @override + /// Set get primaryKey => {recipe, ingredient}; + /// + /// IntColumn get recipe => integer().autoIncrement()(); + /// IntColumn get ingredient => integer().autoIncrement()(); + /// + /// IntColumn get amountInGrams => integer().named('amount')(); + ///} + /// ``` + /// The getter must return a set literal using the `=>` syntax so that the + /// sally generator can understand the code. + /// Also, please not that it's an error to have a + /// [IntColumnBuilder.autoIncrement] column and a custom primary key. + /// Writing such table in sql will throw at runtime. @visibleForOverriding Set get primaryKey => null; diff --git a/sally/lib/src/runtime/migration.dart b/sally/lib/src/runtime/migration.dart index 4375dcac..43fd1fec 100644 --- a/sally/lib/src/runtime/migration.dart +++ b/sally/lib/src/runtime/migration.dart @@ -7,6 +7,10 @@ import 'package:sally/src/runtime/structure/table_info.dart'; typedef Future OnCreate(Migrator m); typedef Future OnUpgrade(Migrator m, int from, int to); +/// Signature of a function that's called after a migration has finished and the +/// database is ready to be used. Useful to populate data. +typedef Future OnMigrationFinished(); + Future _defaultOnCreate(Migrator m) => m.createAllTables(); Future _defaultOnUpdate(Migrator m, int from, int to) async => throw Exception("You've bumped the schema version for your sally database " @@ -21,9 +25,15 @@ class MigrationStrategy { /// happened at a lower [GeneratedDatabase.schemaVersion]. final OnUpgrade onUpgrade; + /// Executes after the database is ready and all migrations ran, but before + /// any other queries will be executed, making this method suitable to + /// populate data. + final OnMigrationFinished onFinished; + MigrationStrategy({ this.onCreate = _defaultOnCreate, this.onUpgrade = _defaultOnUpdate, + this.onFinished, }); } @@ -59,6 +69,19 @@ class Migrator { if (i < table.$columns.length - 1) sql.write(', '); } + if (table.$primaryKey != null) { + sql.write(', PRIMARY KEY ('); + final pkList = table.$primaryKey.toList(growable: false); + for (var i = 0; i < pkList.length; i++) { + final column = pkList[i]; + + sql.write(column.$name); + + if (i != pkList.length - 1) sql.write(', '); + } + sql.write(')'); + } + sql.write(');'); return issueCustomQuery(sql.toString()); diff --git a/sally/test/data/tables/todos.dart b/sally/test/data/tables/todos.dart index 76dbaa54..df6f16ee 100644 --- a/sally/test/data/tables/todos.dart +++ b/sally/test/data/tables/todos.dart @@ -27,7 +27,15 @@ class Categories extends Table { TextColumn get description => text().named('desc')(); } -@UseSally(tables: [TodosTable, Categories, Users]) +class SharedTodos extends Table { + IntColumn get todo => integer()(); + IntColumn get user => integer()(); + + @override + Set get primaryKey => {todo, user}; +} + +@UseSally(tables: [TodosTable, Categories, Users, SharedTodos]) class TodoDb extends _$TodoDb { TodoDb(QueryExecutor e) : super(e); diff --git a/sally/test/data/tables/todos.g.dart b/sally/test/data/tables/todos.g.dart index a0522ddb..0be0d288 100644 --- a/sally/test/data/tables/todos.g.dart +++ b/sally/test/data/tables/todos.g.dart @@ -64,10 +64,8 @@ class $TodosTableTable extends TodosTable GeneratedIntColumn get id => GeneratedIntColumn('id', false, hasAutoIncrement: true); @override - GeneratedTextColumn get title => GeneratedTextColumn( - 'title', - true, - ); + GeneratedTextColumn get title => + GeneratedTextColumn('title', true, minTextLength: 4, maxTextLength: 16); @override GeneratedTextColumn get content => GeneratedTextColumn( 'content', @@ -98,7 +96,7 @@ class $TodosTableTable extends TodosTable targetDate.isAcceptableValue(instance.targetDate, isInserting) && category.isAcceptableValue(instance.category, isInserting); @override - Set get $primaryKey => {}; + Set get $primaryKey => null; @override TodoEntry map(Map data) { return TodoEntry.fromData(data, _db); @@ -173,7 +171,7 @@ class $CategoriesTable extends Categories id.isAcceptableValue(instance.id, isInserting) && description.isAcceptableValue(instance.description, isInserting); @override - Set get $primaryKey => {}; + Set get $primaryKey => null; @override Category map(Map data) { return Category.fromData(data, _db); @@ -231,10 +229,8 @@ class $UsersTable extends Users implements TableInfo { GeneratedIntColumn get id => GeneratedIntColumn('id', false, hasAutoIncrement: true); @override - GeneratedTextColumn get name => GeneratedTextColumn( - 'name', - false, - ); + GeneratedTextColumn get name => + GeneratedTextColumn('name', false, minTextLength: 6, maxTextLength: 32); @override GeneratedBoolColumn get isAwesome => GeneratedBoolColumn( 'is_awesome', @@ -252,7 +248,7 @@ class $UsersTable extends Users implements TableInfo { name.isAcceptableValue(instance.name, isInserting) && isAwesome.isAcceptableValue(instance.isAwesome, isInserting); @override - Set get $primaryKey => {}; + Set get $primaryKey => null; @override User map(Map data) { return User.fromData(data, _db); @@ -274,11 +270,79 @@ class $UsersTable extends Users implements TableInfo { } } +class SharedTodo { + final int todo; + final int user; + SharedTodo({this.todo, this.user}); + factory SharedTodo.fromData(Map data, GeneratedDatabase db) { + final intType = db.typeSystem.forDartType(); + return SharedTodo( + todo: intType.mapFromDatabaseResponse(data['todo']), + user: intType.mapFromDatabaseResponse(data['user']), + ); + } + SharedTodo copyWith({int todo, int user}) => SharedTodo( + todo: todo ?? this.todo, + user: user ?? this.user, + ); + @override + int get hashCode => (todo.hashCode) * 31 + user.hashCode; + @override + bool operator ==(other) => + identical(this, other) || + (other is SharedTodo && other.todo == todo && other.user == user); +} + +class $SharedTodosTable extends SharedTodos + implements TableInfo { + final GeneratedDatabase _db; + $SharedTodosTable(this._db); + @override + GeneratedIntColumn get todo => GeneratedIntColumn( + 'todo', + false, + ); + @override + GeneratedIntColumn get user => GeneratedIntColumn( + 'user', + false, + ); + @override + List get $columns => [todo, user]; + @override + SharedTodos get asDslTable => this; + @override + String get $tableName => 'shared_todos'; + @override + bool validateIntegrity(SharedTodo instance, bool isInserting) => + todo.isAcceptableValue(instance.todo, isInserting) && + user.isAcceptableValue(instance.user, isInserting); + @override + Set get $primaryKey => {todo, user}; + @override + SharedTodo map(Map data) { + return SharedTodo.fromData(data, _db); + } + + @override + Map entityToSql(SharedTodo d) { + final map = {}; + if (d.todo != null) { + map['todo'] = Variable(d.todo); + } + if (d.user != null) { + map['user'] = Variable(d.user); + } + return map; + } +} + abstract class _$TodoDb extends GeneratedDatabase { _$TodoDb(QueryExecutor e) : super(const SqlTypeSystem.withDefaults(), e); $TodosTableTable get todosTable => $TodosTableTable(this); $CategoriesTable get categories => $CategoriesTable(this); $UsersTable get users => $UsersTable(this); + $SharedTodosTable get sharedTodos => $SharedTodosTable(this); @override - List get allTables => [todosTable, categories, users]; + List get allTables => [todosTable, categories, users, sharedTodos]; } diff --git a/sally/test/schema_test.dart b/sally/test/schema_test.dart index 554abad1..bd9fc230 100644 --- a/sally/test/schema_test.dart +++ b/sally/test/schema_test.dart @@ -17,7 +17,7 @@ void main() { test('creates all tables', () async { await Migrator(db, mockQueryExecutor).createAllTables(); - // should create todos, categories and users table + // should create todos, categories, users and shared_todos table verify(mockQueryExecutor.call('CREATE TABLE IF NOT EXISTS todos ' '(id INTEGER PRIMARY KEY AUTOINCREMENT, title VARCHAR NULL, ' 'content VARCHAR NOT NULL, target_date INTEGER NULL, ' @@ -29,6 +29,9 @@ void main() { verify(mockQueryExecutor.call('CREATE TABLE IF NOT EXISTS users ' '(id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR NOT NULL, ' 'is_awesome BOOLEAN NOT NULL CHECK (is_awesome in (0, 1)));')); + + verify(mockQueryExecutor.call('CREATE TABLE IF NOT EXISTS shared_todos ' + '(todo INTEGER NOT NULL, user INTEGER NOT NULL, PRIMARY KEY (todo, user));')); }); test('creates individual tables', () async { diff --git a/sally_flutter/README.md b/sally_flutter/README.md index 9ab8b69a..fcae9584 100644 --- a/sally_flutter/README.md +++ b/sally_flutter/README.md @@ -293,7 +293,7 @@ Please note that a workaround for most on this list exists with custom statement ### Planned for the future These aren't sorted by priority. If you have more ideas or want some features happening soon, let us know by creating an issue! -- Specify primary keys +- Specify custom primary keys ✔️ - Support an simplified update that doesn't need an explicit where based on the primary key - Simple `COUNT(*)` operations (group operations will be much more complicated) - Support default values and expressions diff --git a/sally_flutter/lib/sally_flutter.dart b/sally_flutter/lib/sally_flutter.dart index 3bf6b31e..4b1eaf6c 100644 --- a/sally_flutter/lib/sally_flutter.dart +++ b/sally_flutter/lib/sally_flutter.dart @@ -21,6 +21,7 @@ class FlutterQueryExecutor extends QueryExecutor { final bool logStatements; Database _db; + bool _hadMigration = false; FlutterQueryExecutor({@required this.path, this.logStatements}) : _inDbPath = false; @@ -46,14 +47,25 @@ class FlutterQueryExecutor extends QueryExecutor { resolvedPath, version: databaseInfo.schemaVersion, onCreate: (db, version) { + _hadMigration = true; return databaseInfo.handleDatabaseCreation( executor: (sql) => db.execute(sql), ); }, onUpgrade: (db, from, to) { + _hadMigration = true; return databaseInfo.handleDatabaseVersionChange( executor: (sql) => db.execute(sql), from: from, to: to); }, + onOpen: (db) async { + _db = db; + // the openDatabase future will resolve later, so we can get an instance + // where we can send the queries from the onFinished operation; + final fn = databaseInfo.migration.onFinished; + if (fn != null && _hadMigration) { + await fn(); + } + } ); return true; diff --git a/sally_generator/lib/src/model/specified_table.dart b/sally_generator/lib/src/model/specified_table.dart index c2ee1fea..ffcb57f5 100644 --- a/sally_generator/lib/src/model/specified_table.dart +++ b/sally_generator/lib/src/model/specified_table.dart @@ -17,7 +17,11 @@ class SpecifiedTable { final Set primaryKey; const SpecifiedTable( - {this.fromClass, this.columns, this.sqlName, this.dartTypeName, this.primaryKey}); + {this.fromClass, + this.columns, + this.sqlName, + this.dartTypeName, + this.primaryKey}); } String tableInfoNameForTableClass(ClassElement fromClass) => diff --git a/sally_generator/lib/src/parser/table_parser.dart b/sally_generator/lib/src/parser/table_parser.dart index 904e5e20..6b3d5166 100644 --- a/sally_generator/lib/src/parser/table_parser.dart +++ b/sally_generator/lib/src/parser/table_parser.dart @@ -68,34 +68,47 @@ class TableParser extends ParserBase { return tableName; } - Set _readPrimaryKey(ClassElement element, List columns) { + Set _readPrimaryKey( + ClassElement element, List columns) { final primaryKeyGetter = element.getGetter('primaryKey'); if (primaryKeyGetter == null) { return null; } - final ast = generator.loadElementDeclaration(primaryKeyGetter).node as MethodDeclaration; + final ast = generator.loadElementDeclaration(primaryKeyGetter).node + as MethodDeclaration; final body = ast.body; if (body is! ExpressionFunctionBody) { - generator.errors.add(SallyError(affectedElement: primaryKeyGetter, message: 'This must return a set literal using the => syntax!')); + generator.errors.add(SallyError( + affectedElement: primaryKeyGetter, + message: 'This must return a set literal using the => syntax!')); return null; } final expression = (body as ExpressionFunctionBody).expression; - // set expressions {x, y} are parsed as map literals whose values are an empty - // identifier {x: , y: }. yeah. + // set expressions {x, y} are sometimes parsed as map literals whose values + // are an empty identifier {x: , y: }, but sometimes as proper set literal. + // this is probably due to backwards compatibility. // todo should we support MapLiteral2 to support the experiments discussed there? - if (expression is! MapLiteral) { - generator.errors.add(SallyError(affectedElement: primaryKeyGetter, message: 'This must return a set literal!')); - return null; - } - final mapLiteral = expression as MapLiteral; - final parsedPrimaryKey = {}; - for (var entry in mapLiteral.entries) { - final key = entry.key as Identifier; - final column = columns.singleWhere((column) => column.dartGetterName == key.name); - parsedPrimaryKey.add(column); + if (expression is MapLiteral) { + for (var entry in expression.entries) { + final key = entry.key as Identifier; + final column = + columns.singleWhere((column) => column.dartGetterName == key.name); + parsedPrimaryKey.add(column); + } + } else if (expression is SetLiteral) { + for (var entry in expression.elements) { + final column = columns.singleWhere( + (column) => column.dartGetterName == (entry as Identifier).name); + parsedPrimaryKey.add(column); + } + } else { + generator.errors.add(SallyError( + affectedElement: primaryKeyGetter, + message: 'This must return a set literal!')); + return null; } return parsedPrimaryKey; diff --git a/sally_generator/lib/src/sally_generator.dart b/sally_generator/lib/src/sally_generator.dart index b62e0eb4..3fa45e6b 100644 --- a/sally_generator/lib/src/sally_generator.dart +++ b/sally_generator/lib/src/sally_generator.dart @@ -13,7 +13,7 @@ import 'package:sally_generator/src/writer/database_writer.dart'; import 'package:source_gen/source_gen.dart'; class SallyGenerator extends GeneratorForAnnotation { - final Map _astForLibs = {}; + //final Map _astForLibs = {}; final ErrorStore errors = ErrorStore(); TableParser tableParser; @@ -24,11 +24,12 @@ class SallyGenerator extends GeneratorForAnnotation { final Map _foundTables = {}; ElementDeclarationResult loadElementDeclaration(Element element) { - final result = _astForLibs.putIfAbsent(element.library.name, () { + /*final result = _astForLibs.putIfAbsent(element.library.name, () { // ignore: deprecated_member_use return ParsedLibraryResultImpl.tmp(element.library); - }); - + });*/ + // ignore: deprecated_member_use + final result = ParsedLibraryResultImpl.tmp(element.library); return result.getElementDeclaration(element); } @@ -61,6 +62,20 @@ class SallyGenerator extends GeneratorForAnnotation { } } + if (errors.errors.isNotEmpty) { + print('Warning: There were some errors whily running sally_generator:'); + + for (var error in errors.errors) { + print(error.message); + + if (error.affectedElement != null) { + final span = spanForElement(error.affectedElement); + print('${span.start.toolString}\n${span.highlight()}'); + } + } + errors.errors.clear(); + } + if (_foundTables.isEmpty) return ''; final specifiedDb = diff --git a/sally_generator/lib/src/writer/table_writer.dart b/sally_generator/lib/src/writer/table_writer.dart index bea35b9e..adefdb9e 100644 --- a/sally_generator/lib/src/writer/table_writer.dart +++ b/sally_generator/lib/src/writer/table_writer.dart @@ -44,14 +44,7 @@ class TableWriter { ..write('@override\nString get \$tableName => \'${table.sqlName}\';\n'); _writeValidityCheckMethod(buffer); - - // write primary key getter: Set get $primaryKey => {id}; - final primaryKeyColumns = table.primaryKey.map((c) => c.dartGetterName); - buffer - ..write( - '@override\nSet get \$primaryKey => {') - ..write(primaryKeyColumns.join(', ')) - ..write('};\n'); + _writePrimaryKeyOverride(buffer); _writeMappingMethod(buffer); _writeReverseMappingMethod(buffer); @@ -91,8 +84,17 @@ class TableWriter { final isNullable = column.nullable; final additionalParams = {}; - if (column.hasAI) { - additionalParams['hasAutoIncrement'] = 'true'; + for (var feature in column.features) { + if (feature is AutoIncrement) { + additionalParams['hasAutoIncrement'] = 'true'; + } else if (feature is LimitingTextLength) { + if (feature.minLength != null) { + additionalParams['minTextLength'] = feature.minLength.toString(); + } + if (feature.maxLength != null) { + additionalParams['maxTextLength'] = feature.maxLength.toString(); + } + } } // @override @@ -133,4 +135,24 @@ class TableWriter { buffer..write(validationCode)..write(';\n'); } + + void _writePrimaryKeyOverride(StringBuffer buffer) { + buffer.write('@override\nSet get \$primaryKey => '); + if (table.primaryKey == null) { + buffer.write('null;'); + return; + } + + buffer.write('{'); + final pkList = table.primaryKey.toList(); + for (var i = 0; i < pkList.length; i++) { + final pk = pkList[i]; + + buffer.write(pk.dartGetterName); + if (i != pkList.length - 1) { + buffer.write(', '); + } + } + buffer.write('};\n'); + } } diff --git a/sally_generator/test/parser/parser_test.dart b/sally_generator/test/parser/parser_test.dart index 77bdd2da..17edebe4 100644 --- a/sally_generator/test/parser/parser_test.dart +++ b/sally_generator/test/parser/parser_test.dart @@ -112,8 +112,10 @@ void main() async { }); test('parses custom primary keys', () { - final table = TableParser(generator).parse(testLib.getType('CustomPrimaryKey')); + final table = + TableParser(generator).parse(testLib.getType('CustomPrimaryKey')); expect(table.primaryKey, containsAll(table.columns)); + expect(table.columns.any((column) => column.hasAI), isFalse); }); }