Custom primary keys

This commit is contained in:
Simon Binder 2019-03-06 21:43:16 +01:00
parent 83f12a71b6
commit 2e7c079e4d
No known key found for this signature in database
GPG Key ID: B807FDF954BA00CF
15 changed files with 596 additions and 55 deletions

View File

@ -1,5 +1,7 @@
import 'package:sally/sally.dart'; import 'package:sally/sally.dart';
part 'example.g.dart';
// Define tables that can model a database of recipes. // Define tables that can model a database of recipes.
@DataClassName('Category') @DataClassName('Category')
@ -22,7 +24,6 @@ class Ingredients extends Table {
} }
class IngredientInRecipes extends Table { class IngredientInRecipes extends Table {
@override @override
String get tableName => 'recipe_ingredients'; String get tableName => 'recipe_ingredients';
@ -30,8 +31,22 @@ class IngredientInRecipes extends Table {
@override @override
Set<Column> get primaryKey => {recipe, ingredient}; Set<Column> get primaryKey => {recipe, ingredient};
IntColumn get recipe => integer().autoIncrement()(); IntColumn get recipe => integer()();
IntColumn get ingredient => integer().autoIncrement()(); IntColumn get ingredient => integer()();
IntColumn get amountInGrams => integer().named('amount')(); 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'));
});
}

View File

@ -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<String, dynamic> data, GeneratedDatabase db) {
final intType = db.typeSystem.forDartType<int>();
final stringType = db.typeSystem.forDartType<String>();
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<Categories, Category> {
final GeneratedDatabase _db;
$CategoriesTable(this._db);
@override
GeneratedIntColumn get id =>
GeneratedIntColumn('id', false, hasAutoIncrement: true);
@override
GeneratedTextColumn get description => GeneratedTextColumn(
'description',
true,
);
@override
List<GeneratedColumn> 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<GeneratedColumn> get $primaryKey => null;
@override
Category map(Map<String, dynamic> data) {
return Category.fromData(data, _db);
}
@override
Map<String, Variable> entityToSql(Category d) {
final map = <String, Variable>{};
if (d.id != null) {
map['id'] = Variable<int, IntType>(d.id);
}
if (d.description != null) {
map['description'] = Variable<String, StringType>(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<String, dynamic> data, GeneratedDatabase db) {
final intType = db.typeSystem.forDartType<int>();
final stringType = db.typeSystem.forDartType<String>();
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<Recipes, Recipe> {
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<GeneratedColumn> 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<GeneratedColumn> get $primaryKey => null;
@override
Recipe map(Map<String, dynamic> data) {
return Recipe.fromData(data, _db);
}
@override
Map<String, Variable> entityToSql(Recipe d) {
final map = <String, Variable>{};
if (d.id != null) {
map['id'] = Variable<int, IntType>(d.id);
}
if (d.title != null) {
map['title'] = Variable<String, StringType>(d.title);
}
if (d.instructions != null) {
map['instructions'] = Variable<String, StringType>(d.instructions);
}
if (d.category != null) {
map['category'] = Variable<int, IntType>(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<String, dynamic> data, GeneratedDatabase db) {
final intType = db.typeSystem.forDartType<int>();
final stringType = db.typeSystem.forDartType<String>();
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<Ingredients, Ingredient> {
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<GeneratedColumn> 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<GeneratedColumn> get $primaryKey => null;
@override
Ingredient map(Map<String, dynamic> data) {
return Ingredient.fromData(data, _db);
}
@override
Map<String, Variable> entityToSql(Ingredient d) {
final map = <String, Variable>{};
if (d.id != null) {
map['id'] = Variable<int, IntType>(d.id);
}
if (d.name != null) {
map['name'] = Variable<String, StringType>(d.name);
}
if (d.caloriesPer100g != null) {
map['calories'] = Variable<int, IntType>(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<String, dynamic> data, GeneratedDatabase db) {
final intType = db.typeSystem.forDartType<int>();
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<IngredientInRecipes, IngredientInRecipe> {
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<GeneratedColumn> 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<GeneratedColumn> get $primaryKey => {recipe, ingredient};
@override
IngredientInRecipe map(Map<String, dynamic> data) {
return IngredientInRecipe.fromData(data, _db);
}
@override
Map<String, Variable> entityToSql(IngredientInRecipe d) {
final map = <String, Variable>{};
if (d.recipe != null) {
map['recipe'] = Variable<int, IntType>(d.recipe);
}
if (d.ingredient != null) {
map['ingredient'] = Variable<int, IntType>(d.ingredient);
}
if (d.amountInGrams != null) {
map['amount'] = Variable<int, IntType>(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<TableInfo> get allTables =>
[categories, recipes, ingredients, ingredientInRecipes];
}

View File

@ -40,10 +40,6 @@ class ColumnBuilder<Builder, ResultColumn> {
/// `IntColumn get id = integer((c) => c.named('user_id'))`. /// `IntColumn get id = integer((c) => c.named('user_id'))`.
Builder named(String name) => null; 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 /// Marks this column as nullable. Nullable columns should not appear in a
/// primary key. Columns are non-null by default. /// primary key. Columns are non-null by default.
Builder nullable() => null; Builder nullable() => null;

View File

@ -17,8 +17,23 @@ abstract class Table {
@visibleForOverriding @visibleForOverriding
String get tableName => null; String get tableName => null;
/// In the future, you can override this to specify a custom primary key. This /// Override this to specify custom primary keys:
/// is not supported by sally at the moment. /// ```dart
/// class IngredientInRecipes extends Table {
/// @override
/// Set<Column> 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 @visibleForOverriding
Set<Column> get primaryKey => null; Set<Column> get primaryKey => null;

View File

@ -7,6 +7,10 @@ import 'package:sally/src/runtime/structure/table_info.dart';
typedef Future<void> OnCreate(Migrator m); typedef Future<void> OnCreate(Migrator m);
typedef Future<void> OnUpgrade(Migrator m, int from, int to); typedef Future<void> 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<void> OnMigrationFinished();
Future<void> _defaultOnCreate(Migrator m) => m.createAllTables(); Future<void> _defaultOnCreate(Migrator m) => m.createAllTables();
Future<void> _defaultOnUpdate(Migrator m, int from, int to) async => Future<void> _defaultOnUpdate(Migrator m, int from, int to) async =>
throw Exception("You've bumped the schema version for your sally database " throw Exception("You've bumped the schema version for your sally database "
@ -21,9 +25,15 @@ class MigrationStrategy {
/// happened at a lower [GeneratedDatabase.schemaVersion]. /// happened at a lower [GeneratedDatabase.schemaVersion].
final OnUpgrade onUpgrade; 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({ MigrationStrategy({
this.onCreate = _defaultOnCreate, this.onCreate = _defaultOnCreate,
this.onUpgrade = _defaultOnUpdate, this.onUpgrade = _defaultOnUpdate,
this.onFinished,
}); });
} }
@ -59,6 +69,19 @@ class Migrator {
if (i < table.$columns.length - 1) sql.write(', '); 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(');'); sql.write(');');
return issueCustomQuery(sql.toString()); return issueCustomQuery(sql.toString());

View File

@ -27,7 +27,15 @@ class Categories extends Table {
TextColumn get description => text().named('desc')(); 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<Column> get primaryKey => {todo, user};
}
@UseSally(tables: [TodosTable, Categories, Users, SharedTodos])
class TodoDb extends _$TodoDb { class TodoDb extends _$TodoDb {
TodoDb(QueryExecutor e) : super(e); TodoDb(QueryExecutor e) : super(e);

View File

@ -64,10 +64,8 @@ class $TodosTableTable extends TodosTable
GeneratedIntColumn get id => GeneratedIntColumn get id =>
GeneratedIntColumn('id', false, hasAutoIncrement: true); GeneratedIntColumn('id', false, hasAutoIncrement: true);
@override @override
GeneratedTextColumn get title => GeneratedTextColumn( GeneratedTextColumn get title =>
'title', GeneratedTextColumn('title', true, minTextLength: 4, maxTextLength: 16);
true,
);
@override @override
GeneratedTextColumn get content => GeneratedTextColumn( GeneratedTextColumn get content => GeneratedTextColumn(
'content', 'content',
@ -98,7 +96,7 @@ class $TodosTableTable extends TodosTable
targetDate.isAcceptableValue(instance.targetDate, isInserting) && targetDate.isAcceptableValue(instance.targetDate, isInserting) &&
category.isAcceptableValue(instance.category, isInserting); category.isAcceptableValue(instance.category, isInserting);
@override @override
Set<GeneratedColumn> get $primaryKey => <GeneratedColumn>{}; Set<GeneratedColumn> get $primaryKey => null;
@override @override
TodoEntry map(Map<String, dynamic> data) { TodoEntry map(Map<String, dynamic> data) {
return TodoEntry.fromData(data, _db); return TodoEntry.fromData(data, _db);
@ -173,7 +171,7 @@ class $CategoriesTable extends Categories
id.isAcceptableValue(instance.id, isInserting) && id.isAcceptableValue(instance.id, isInserting) &&
description.isAcceptableValue(instance.description, isInserting); description.isAcceptableValue(instance.description, isInserting);
@override @override
Set<GeneratedColumn> get $primaryKey => <GeneratedColumn>{}; Set<GeneratedColumn> get $primaryKey => null;
@override @override
Category map(Map<String, dynamic> data) { Category map(Map<String, dynamic> data) {
return Category.fromData(data, _db); return Category.fromData(data, _db);
@ -231,10 +229,8 @@ class $UsersTable extends Users implements TableInfo<Users, User> {
GeneratedIntColumn get id => GeneratedIntColumn get id =>
GeneratedIntColumn('id', false, hasAutoIncrement: true); GeneratedIntColumn('id', false, hasAutoIncrement: true);
@override @override
GeneratedTextColumn get name => GeneratedTextColumn( GeneratedTextColumn get name =>
'name', GeneratedTextColumn('name', false, minTextLength: 6, maxTextLength: 32);
false,
);
@override @override
GeneratedBoolColumn get isAwesome => GeneratedBoolColumn( GeneratedBoolColumn get isAwesome => GeneratedBoolColumn(
'is_awesome', 'is_awesome',
@ -252,7 +248,7 @@ class $UsersTable extends Users implements TableInfo<Users, User> {
name.isAcceptableValue(instance.name, isInserting) && name.isAcceptableValue(instance.name, isInserting) &&
isAwesome.isAcceptableValue(instance.isAwesome, isInserting); isAwesome.isAcceptableValue(instance.isAwesome, isInserting);
@override @override
Set<GeneratedColumn> get $primaryKey => <GeneratedColumn>{}; Set<GeneratedColumn> get $primaryKey => null;
@override @override
User map(Map<String, dynamic> data) { User map(Map<String, dynamic> data) {
return User.fromData(data, _db); return User.fromData(data, _db);
@ -274,11 +270,79 @@ class $UsersTable extends Users implements TableInfo<Users, User> {
} }
} }
class SharedTodo {
final int todo;
final int user;
SharedTodo({this.todo, this.user});
factory SharedTodo.fromData(Map<String, dynamic> data, GeneratedDatabase db) {
final intType = db.typeSystem.forDartType<int>();
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<SharedTodos, SharedTodo> {
final GeneratedDatabase _db;
$SharedTodosTable(this._db);
@override
GeneratedIntColumn get todo => GeneratedIntColumn(
'todo',
false,
);
@override
GeneratedIntColumn get user => GeneratedIntColumn(
'user',
false,
);
@override
List<GeneratedColumn> 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<GeneratedColumn> get $primaryKey => {todo, user};
@override
SharedTodo map(Map<String, dynamic> data) {
return SharedTodo.fromData(data, _db);
}
@override
Map<String, Variable> entityToSql(SharedTodo d) {
final map = <String, Variable>{};
if (d.todo != null) {
map['todo'] = Variable<int, IntType>(d.todo);
}
if (d.user != null) {
map['user'] = Variable<int, IntType>(d.user);
}
return map;
}
}
abstract class _$TodoDb extends GeneratedDatabase { abstract class _$TodoDb extends GeneratedDatabase {
_$TodoDb(QueryExecutor e) : super(const SqlTypeSystem.withDefaults(), e); _$TodoDb(QueryExecutor e) : super(const SqlTypeSystem.withDefaults(), e);
$TodosTableTable get todosTable => $TodosTableTable(this); $TodosTableTable get todosTable => $TodosTableTable(this);
$CategoriesTable get categories => $CategoriesTable(this); $CategoriesTable get categories => $CategoriesTable(this);
$UsersTable get users => $UsersTable(this); $UsersTable get users => $UsersTable(this);
$SharedTodosTable get sharedTodos => $SharedTodosTable(this);
@override @override
List<TableInfo> get allTables => [todosTable, categories, users]; List<TableInfo> get allTables => [todosTable, categories, users, sharedTodos];
} }

View File

@ -17,7 +17,7 @@ void main() {
test('creates all tables', () async { test('creates all tables', () async {
await Migrator(db, mockQueryExecutor).createAllTables(); 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 ' verify(mockQueryExecutor.call('CREATE TABLE IF NOT EXISTS todos '
'(id INTEGER PRIMARY KEY AUTOINCREMENT, title VARCHAR NULL, ' '(id INTEGER PRIMARY KEY AUTOINCREMENT, title VARCHAR NULL, '
'content VARCHAR NOT NULL, target_date INTEGER NULL, ' 'content VARCHAR NOT NULL, target_date INTEGER NULL, '
@ -29,6 +29,9 @@ void main() {
verify(mockQueryExecutor.call('CREATE TABLE IF NOT EXISTS users ' verify(mockQueryExecutor.call('CREATE TABLE IF NOT EXISTS users '
'(id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR NOT NULL, ' '(id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR NOT NULL, '
'is_awesome BOOLEAN NOT NULL CHECK (is_awesome in (0, 1)));')); '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 { test('creates individual tables', () async {

View File

@ -293,7 +293,7 @@ Please note that a workaround for most on this list exists with custom statement
### Planned for the future ### Planned for the future
These aren't sorted by priority. If you have more ideas or want some features happening soon, These aren't sorted by priority. If you have more ideas or want some features happening soon,
let us know by creating an issue! 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 - 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) - Simple `COUNT(*)` operations (group operations will be much more complicated)
- Support default values and expressions - Support default values and expressions

View File

@ -21,6 +21,7 @@ class FlutterQueryExecutor extends QueryExecutor {
final bool logStatements; final bool logStatements;
Database _db; Database _db;
bool _hadMigration = false;
FlutterQueryExecutor({@required this.path, this.logStatements}) FlutterQueryExecutor({@required this.path, this.logStatements})
: _inDbPath = false; : _inDbPath = false;
@ -46,14 +47,25 @@ class FlutterQueryExecutor extends QueryExecutor {
resolvedPath, resolvedPath,
version: databaseInfo.schemaVersion, version: databaseInfo.schemaVersion,
onCreate: (db, version) { onCreate: (db, version) {
_hadMigration = true;
return databaseInfo.handleDatabaseCreation( return databaseInfo.handleDatabaseCreation(
executor: (sql) => db.execute(sql), executor: (sql) => db.execute(sql),
); );
}, },
onUpgrade: (db, from, to) { onUpgrade: (db, from, to) {
_hadMigration = true;
return databaseInfo.handleDatabaseVersionChange( return databaseInfo.handleDatabaseVersionChange(
executor: (sql) => db.execute(sql), from: from, to: to); 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; return true;

View File

@ -17,7 +17,11 @@ class SpecifiedTable {
final Set<SpecifiedColumn> primaryKey; final Set<SpecifiedColumn> primaryKey;
const SpecifiedTable( 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) => String tableInfoNameForTableClass(ClassElement fromClass) =>

View File

@ -68,35 +68,48 @@ class TableParser extends ParserBase {
return tableName; return tableName;
} }
Set<SpecifiedColumn> _readPrimaryKey(ClassElement element, List<SpecifiedColumn> columns) { Set<SpecifiedColumn> _readPrimaryKey(
ClassElement element, List<SpecifiedColumn> columns) {
final primaryKeyGetter = element.getGetter('primaryKey'); final primaryKeyGetter = element.getGetter('primaryKey');
if (primaryKeyGetter == null) { if (primaryKeyGetter == null) {
return null; return null;
} }
final ast = generator.loadElementDeclaration(primaryKeyGetter).node as MethodDeclaration; final ast = generator.loadElementDeclaration(primaryKeyGetter).node
as MethodDeclaration;
final body = ast.body; final body = ast.body;
if (body is! ExpressionFunctionBody) { 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; return null;
} }
final expression = (body as ExpressionFunctionBody).expression; final expression = (body as ExpressionFunctionBody).expression;
// set expressions {x, y} are parsed as map literals whose values are an empty // set expressions {x, y} are sometimes parsed as map literals whose values
// identifier {x: , y: }. yeah. // 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? // 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 = <SpecifiedColumn>{}; final parsedPrimaryKey = <SpecifiedColumn>{};
for (var entry in mapLiteral.entries) { if (expression is MapLiteral) {
for (var entry in expression.entries) {
final key = entry.key as Identifier; final key = entry.key as Identifier;
final column = columns.singleWhere((column) => column.dartGetterName == key.name); final column =
columns.singleWhere((column) => column.dartGetterName == key.name);
parsedPrimaryKey.add(column); 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; return parsedPrimaryKey;
} }

View File

@ -13,7 +13,7 @@ import 'package:sally_generator/src/writer/database_writer.dart';
import 'package:source_gen/source_gen.dart'; import 'package:source_gen/source_gen.dart';
class SallyGenerator extends GeneratorForAnnotation<UseSally> { class SallyGenerator extends GeneratorForAnnotation<UseSally> {
final Map<String, ParsedLibraryResult> _astForLibs = {}; //final Map<String, ParsedLibraryResult> _astForLibs = {};
final ErrorStore errors = ErrorStore(); final ErrorStore errors = ErrorStore();
TableParser tableParser; TableParser tableParser;
@ -24,11 +24,12 @@ class SallyGenerator extends GeneratorForAnnotation<UseSally> {
final Map<DartType, SpecifiedTable> _foundTables = {}; final Map<DartType, SpecifiedTable> _foundTables = {};
ElementDeclarationResult loadElementDeclaration(Element element) { ElementDeclarationResult loadElementDeclaration(Element element) {
final result = _astForLibs.putIfAbsent(element.library.name, () { /*final result = _astForLibs.putIfAbsent(element.library.name, () {
// ignore: deprecated_member_use // ignore: deprecated_member_use
return ParsedLibraryResultImpl.tmp(element.library); return ParsedLibraryResultImpl.tmp(element.library);
}); });*/
// ignore: deprecated_member_use
final result = ParsedLibraryResultImpl.tmp(element.library);
return result.getElementDeclaration(element); return result.getElementDeclaration(element);
} }
@ -61,6 +62,20 @@ class SallyGenerator extends GeneratorForAnnotation<UseSally> {
} }
} }
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 ''; if (_foundTables.isEmpty) return '';
final specifiedDb = final specifiedDb =

View File

@ -44,14 +44,7 @@ class TableWriter {
..write('@override\nString get \$tableName => \'${table.sqlName}\';\n'); ..write('@override\nString get \$tableName => \'${table.sqlName}\';\n');
_writeValidityCheckMethod(buffer); _writeValidityCheckMethod(buffer);
_writePrimaryKeyOverride(buffer);
// write primary key getter: Set<Column> get $primaryKey => <GeneratedColumn>{id};
final primaryKeyColumns = table.primaryKey.map((c) => c.dartGetterName);
buffer
..write(
'@override\nSet<GeneratedColumn> get \$primaryKey => <GeneratedColumn>{')
..write(primaryKeyColumns.join(', '))
..write('};\n');
_writeMappingMethod(buffer); _writeMappingMethod(buffer);
_writeReverseMappingMethod(buffer); _writeReverseMappingMethod(buffer);
@ -91,8 +84,17 @@ class TableWriter {
final isNullable = column.nullable; final isNullable = column.nullable;
final additionalParams = <String, String>{}; final additionalParams = <String, String>{};
if (column.hasAI) { for (var feature in column.features) {
if (feature is AutoIncrement) {
additionalParams['hasAutoIncrement'] = 'true'; 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 // @override
@ -133,4 +135,24 @@ class TableWriter {
buffer..write(validationCode)..write(';\n'); buffer..write(validationCode)..write(';\n');
} }
void _writePrimaryKeyOverride(StringBuffer buffer) {
buffer.write('@override\nSet<GeneratedColumn> 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');
}
} }

View File

@ -112,8 +112,10 @@ void main() async {
}); });
test('parses custom primary keys', () { 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.primaryKey, containsAll(table.columns));
expect(table.columns.any((column) => column.hasAI), isFalse);
}); });
} }