keg is small and handy barrel for beer
keg_generator is code generator package to simplify access to local SQLite file.
Generated code uses sqflite to access SQLite file.
Tested on sqflite version 2.4.2.
Is this kind of Object-Relational Mapping? I'm not sure about it.
Following is sample code to define simple database.
sole_table_test.dart
import 'package:keg_annotation/keg_annotation.dart';
import 'package:sqflite/sqflite.dart';
part 'sole_table_test.g.dart';
@table
class User {
int id;
String name;
User(this.name, [this.id = 0]);
Map<String, Object?> toSqlMap() => _$UserHelper.toSqlMap(this);
factory User.fromSqlMap(Map<String, Object?> map) =>
_$UserHelper.fromSqlMap(map);
}
@KegDatabase(tables: [User])
class AppDatabase extends _$AppDatabase {
@override
Future<String> getPathToOpen() async {
return 'app.db';
}
}
Class User is annotated with @table and referenced to define user table.
Class AppDatabase is annotated with @KegDatabase
and used to define SQLite file.
getPathToOpen is used to determine SQLite file path.
_$UserHelper and _$AppDatabase classes are generated by keg_generator.
And CRUD operations are,
final appdb = AppDatabase();
await appdb.open();
User user1 = User('John');
print(user1.id); // 0
await appdb.registerUser(user1);
print(user1.id); // 1
List<User> result = await appdb.queryUser(
where: "${appdb.userHelper.column.name} = ?",
whereArgs: ['John'],
);
await appdb.deleteUserByIds([user1]);
await appdb.close();
First you need to call open to open SQLite file.
If you want to create in memory database, call openInMemory instead.
registerUser is used for insert or update user record to table.
If id of the user is 0, record is inserted,
and id is assigned to value by AUTOINCREMENT data type of SQLite.
If id is not 0, REPLACE statement is used,
so record is updated if record with the id exists,
or inserted if not exist.
queryUser is to query users, and returns list of User.
queryUser parameters are similar to
sqflite query.
appdb.userHelper is instance of _$UserHelper class generated by keg_generator,
and it holds table name and column names.
class _$UserHelper {
final String tableName = '"user"';
final column = (id: '"id"', name: '"name"');
...
}
These names can be used on query or delete methods.
Table name and column names are converted to snake_case. For Example,
@table
class ItemInfo {
int id;
...
bool isActive;
...
}
class _$ItemInfoHelper {
final String tableName = '"item_info"';
final column = (
id: '"id"',
...
isActive: '"is_active"',
);
...
}
deleteUserByIds is to delete user records by list of User
Full list of generated methods are described here.
Table of Contents
- Getting Started
- Example
- Limitations
- Features
- Generated Codes Detail
- Additional Information
Getting started
Packages you need is, keg_annotation, keg_generator(this package), build_runner, and sqflite (or sqflite_common_ffi).
> flutter pub add keg_annotation dev:keg_generator dev:build_runner
> flutter pub add sqflite
After create dart file like sample on top,
following command will generate .g.dart file.
> dart run build_runner build
Or if you want to generate automatically every time when code is modified,
> dart run build_runner watch
Example
Flutter example project using keg_generator is in github repository.
It also includes
widget test example.
Widget tests cannot access local files, it have to open in memory database.
Limitations
Table classes must have integer id field and it's SQLite type is INTEGER PRIMARY KEY AUTOINCREMENT.
Field types of table classes are limited as follows,
| Field type | SQLite Data Type |
|---|---|
| int | INTEGER |
| double | REAL |
| bool | INTEGER |
| String | TEXT |
| DateTime | INTEGER |
| enum | TEXT |
Optional(nullable) type is not supported, except table relationships.
List and any other collection types are not supported, except table relationships.
On migration, only followings are allowed.
- Add field on table class
- Add table class
Features
Fields in Table Classes and Ignore Annotation
In general, fields in table classes are stored in SQLite tables.
Exceptions are,
- static fields
- private fields
- fields annotated by
Ignore.
ignore_test.dart
class User {
int id;
String name;
final String key = 'abcde';
static String staticField = 'hijkl';
@ignore
String sir;
String tagStr = '';
List<String> _tagList = [];
...
}
In this example, stored fields are id, name, key and tagStr.
If both setter and getter methods are defined, it is regarded as field and stored in SQLite table.
class User {
...
@ignore
List<String> get tagList {
// final ret = tagStr.split(',');
// return ret.map((e) => e.trim()).toList();
return _tagList;
}
//@ignore
set tagList(List<String> value) {
// _tagStr = value.join(',');
_tagList = value;
}
String get getterOnly => 'Xyz';
set setterOnly(String value) {
// do nothing
}
...
}
In this example, tagList is stored to sqlite table if ignore annotation is not set.
ignore annotation is required on either getter or setter.
getterOnly and setterOnly is not stored, because no setter on getterOnly, and not getter on setterOnly.
One to Many Relationship
Following is sample code to define one to many relationship, that one Category to many Items.
one_to_many_test.dart
@table
class Category {
int id;
String name;
@BackLink(to: "category", order: "name", descendant: true)
List<Item> itemList;
Category(this.name, {this.id = 0, this.itemList = const []});
Map<String, Object?> toSqlMap() => _$CategoryHelper.toSqlMap(this);
factory Category.fromSqlMap(Map<String, Object?> map) =>
_$CategoryHelper.fromSqlMap(map);
}
@table
class Item {
int id;
String name;
Category? category;
Item(this.name, {this.category, this.id = 0});
Map<String, Object?> toSqlMap() => _$ItemHelper.toSqlMap(this);
factory Item.fromSqlMap(Map<String, Object?> map) =>
_$ItemHelper.fromSqlMap(map);
}
@KegDatabase(tables: [Category, Item])
class AppDatabase extends _$AppDatabase {
@override
Future<String> getPathToOpen() async {
return 'one2many.db';
}
@override
Future<void> onConfigure(Database db) async {
await db.execute('PRAGMA foreign_keys = ON');
}
}
If you want foreign key constraints, set on onCofigure method.
Note that Item has Category field and it must be optional.
And category must be optional parameter on Item constructor.
(In order to support back link and avoid infinite recursive call between link and back link)
Back link can be defined if you require, with @BackLink annotation.
to parameter of BackLink is field name of Item
that to wchich link this back link is.
order parameter to define order of item lists by field name of Item.
It is optional and default order is by id.
If descendant parameter is true, ordered by descendant order.
It is optional and default is ascendant order.
Note back link is to acquire by query methods.
registerCategory do nothing on itemList.
Sample code for CRUD operations are,
final cat = Category('pen');
await appdb.registerCategory(cat);
final item1 = Item('ballpoint pen', category: cat);
await appdb.registerItem(item1);
final batch4 = appdb.batch();
batch4.queryCategory();
batch4.queryItem();
final result4 = await batch4.commit();
await appdb.deleteItemByIds([item1]);
await appdb.deleteCategoryByIds([cat]);
Many to Many Relationship
Following is sample code to define many to many relationship.
To define many to many, middle table is required.
many_to_many.dart
@table
class Order {
int id;
String user;
@ManyToMany(middle: OrderToItem, self: 'order', target: 'item',
order: 'name', descendant: true)
List<Item> itemList = [];
@ManyToMany(middle: OrderToItem, self: 'order', target: 'item',
order: 'name', descendant: true)
List<Item> itemList2 = [];
Order(this.user, {this.id = 0});
...
}
@table
class Item {
int id;
String name;
@BackLink(to: 'itemList')
List <Order> orderList = [];
Item(this.name, {this.id = 0});
...
}
@table
class OrderToItem {
int id;
Order? order;
String field;
Item? item;
OrderToItem({required this.field, this.order, this.item, this.id = 0});
...
}
@KegDatabase(tables: [Order, Item, OrderToItem])
class AppDatabase extends _$AppDatabase {
@override
Future<String> getPathToOpen() async {
// Implement your logic to get the database path
return 'many2many.db';
}
@override
Future<void> onConfigure(Database db) async {
await db.execute('PRAGMA foreign_keys = ON');
//return super.onConfigure(db);
}
}
Middle table class OrderToItem has field named field,
value of which is field name of Order. (itemList or itemList2 in this sample)
BackLink annotation is same as one to many relationship.
On ManyToMany annotation, middle parameter is table class in middle.
self parameter is field name in middle table class which holds source instance.
target parameter is field name in middle table class which holds target instance.
Back link is for query methods, so registerItem do nothing on orderList.
On the other hand, registerOrder records Order-Item relationship into SQLite file.
Acquirement on many to many relation and back link is one way.
If you acquire Order by queryOrder, Order.itemList is acquired, but
Order.itemList[n].orderList is always empty.
And if you acquire Item by queryItem, Item.orderList is acquired, but
Item.orderList[n].itemList is always empty.
Otherwise cause infinite recursive loop.
Migration
Migration supported is only add field to table class or add table class to database.
Following is sample code for migration.
Before migtation(migration1_test.dart):
@table
class ItemInfo {
int id;
@unique
String name;
ItemInfo(this.name, {this.id = 0,});
...
}
@KegDatabase(tables: [ItemInfo], schemaVersion: 1)
class AppDatabase extends _$AppDatabase {
...
}
After migration(migration1_after.dart):
@table
class User {
int id;
String name;
User(this.name, [this.id = 0]);
...
}
enum Color { red, green, blue }
@table()
class ItemInfo {
int id;
@unique
String name;
int stock;
@index
Color color;
double weight;
bool isActive;
DateTime created = DateTime.now();
ItemInfo(
this.name, {
required this.weight,
required this.color,
this.stock = 0,
this.isActive = true,
this.id = 0,
});
...
}
@KegDatabase(tables: [User, ItemInfo], schemaVersion: 2)
class AppDatabase extends _$AppDatabase {
...
}
This sample adds fields in ItemInfo class, and add User class.
You also need to update @KegDatabase to increment schemaVersion.
To generate migration code,
keg_generator needs to know which field is added to table class.
keg_generator refers to previouly generated .g.dart file to find fields.
migration1_before.g.dart
class _$ItemInfoHelper {
...
static final v1ColumnList = ['id', 'name', 'weight'];
...
}
But when you need to delete generated code and re-generate from scratch,
migration information will be lost.
To avoid the situation, you can copy vnColumnList to table class,
then keg_generator refers it.
migration2_after.dart
@table
class ItemInfo {
static final v1ColumnList = ['id', 'name', 'weight'];
int id;
String name;
int stock;
Color color;
double weight;
bool isActive;
DateTime created = DateTime.now();
ItemInfo(
this.name, {
required this.weight,
required this.color,
this.stock = 0,
this.isActive = true,
this.id = 0,
});
...
}
Transaction and Batch
Transaction is supported as same as sqflite.
transaction_test.dart
final user1 = User('Mike');
final user2 = User('Jack');
await appdb.transaction((txn) async {
await txn.registerUser(user1);
await txn.registerUser(user2);
});
Batch is also same.
batch_test.dart
final batch1 = appdb.batch();
batch1.queryUser();
final result1 = await batch1.commit();
Note when noResult parameter of batch commit is true,
registerTable throws StateError when id is 0.
I added onCommit function parameter on batch methods.
final user1 = User('Mike');
final batch1 = appdb.batch();
batch1.registerUser(user1, (noResult, object) async {
expect(noResult, null);
expect(object, isA<int>());
expect(user1.id, object);
return object;
});
batch1.queryUser(onCommit: (noResult, object) async {
expect(noResult, null);
expect(object, isA<List<User>>());
final list = object as List<User>;
expect(list.length, 1);
expect(list[0].name, user1.name);
return object;
});
await batch1.commit();
onCommit fuction is called after batch commit.
First parameter noResult of onCommit function is
as same as noResult parameter of batch commit.
Second parameter object is corresponding return value of batch commit.
Index
SQLite index is to improve performance on query function.
index_test.dart
@table
class User {
int id;
@unique
String name;
@Index(unique: false, descendant: true)
DateTime updated = DateTime.now();
@index
String contact;
User(this.name, {this.contact = '', this.id = 0});
Map<String, Object?> toSqlMap() => _$UserHelper.toSqlMap(this);
factory User.fromSqlMap(Map<String, Object?> map) =>
_$UserHelper.fromSqlMap(map);
}
This sample makes 3 indexes, user_name_idx, user_updated_idx and user_contact_idx.
You can create normal index by @index annotation,
unique index by @unique annotation.
If descendant index required, use @Index annotation with descendant to true.
Use with json_serializable and freezed annotation.
It is able to use json_serializable and freezed annotation on table class if you want.
Sample with json_serializable : (serializable_test.dart)
@table
@JsonSerializable()
class Category {
int id;
String name;
@BackLink(to: "category", order: "name")
@JsonKey(fromJson: _itemListFromJson, toJson: _itemListToJson)
final List<Item> itemList;
Category(this.name, {this.id = 0, this.itemList = const []});
Map<String, Object?> toSqlMap() => _$CategoryHelper.toSqlMap(this);
factory Category.fromSqlMap(Map<String, Object?> map) =>
_$CategoryHelper.fromSqlMap(map);
factory Category.fromJson(Map<String, dynamic> json)
=> _$CategoryFromJson(json);
Map<String, dynamic> toJson() => _$CategoryToJson(this);
static List<Item> _itemListFromJson(List<dynamic> json) {
return json.map((e) => Item.fromJson(e as Map<String, dynamic>)).toList();
}
static List<dynamic> _itemListToJson(List<Item> itemList) {
return itemList.map((e) => e.toJson()).toList();
}
}
Sample with json_serializable and freezed : (freezed_test.dart)
@table
@freezed
@JsonSerializable()
class Category with _$Category {
@override
final int id;
@override
final String name;
@override
@BackLink(to: "category", order: "name")
@JsonKey(fromJson: _itemListFromJson, toJson: _itemListToJson)
final List<Item> itemList;
const Category(this.name, {this.id = 0, this.itemList = const []});
//const Category(this.name, {this.id = 0});
Map<String, Object?> toSqlMap() => _$CategoryHelper.toSqlMap(this);
factory Category.fromSqlMap(Map<String, Object?> map) =>
_$CategoryHelper.fromSqlMap(map);
factory Category.fromJson(Map<String, dynamic> json)
=> _$CategoryFromJson(json);
Map<String, dynamic> toJson() => _$CategoryToJson(this);
static List<Item> _itemListFromJson(List<dynamic> json) {
return json.map((e) => Item.fromJson(e as Map<String, dynamic>)).toList();
}
static List<dynamic> _itemListToJson(List<Item> itemList) {
return itemList.map((e) => e.toJson()).toList();
}
}
When use with freezed, it is not able to set id automatically.
You must set id manually.
var item = Item('ballpoint pen', category: cat);
final itemId = await appdb.registerItem(item);
item = item.copyWith(id: itemId);
Generated Codes Detail
With sample code on page top, classes generated are,
- _$AppDatabase
- _$UserHelper
- _$AppDatabaseExecutor
- _$AppDatabaseTransactionWrapper
- _$AppDatabaseBatchWrapper
where AppDatabase and User is classes defined in sample code.
_$AppDatabase
This is main class you use.
This class is generated per database class.
abstract class _$AppDatabase implements _$AppDatabaseExecutor
_$AppDatabase Fields
late Database database
Object of sqflite Database class.
late final _$TableHelper tableHelper
Instance of table helper class.
_$AppDatabase CRUD methods
For each table classes, following methods are defined.
registerTable
Future<int> registerTable(Table item)
Insert or update record to SQLite table.
Always update every columns in record.
If id is 0, record is inserted.
If id is not 0, REPLACE statement is used.
If record with the id exists in the table, is updated.
If record with the id does not exist, is inserted.
queryTable
Future<List<Table>> queryTable({
String? where,
List<Object?>? whereArgs,
String? orderBy,
int? limit,
int? offset,
List<({String table, String column})> dropKeys = const [],
})
Query records from SQLite table and returns list of table class objects.
Most parameters are same as sqflite query.
dropKeys parameter is mainly for internal use.
If you have linked tables and do not need link information,
you can improve performance of query.
DropKeys example(one_to_many_test.dart) :
final dropKey = (
table: appdb.itemHelper.tableName,
column: appdb.itemHelper.column.category,
);
final result = await appdb.queryItem(dropKeys: [dropKey]);
If column specified in dropKeys is mandatory parameter of constructor, Exception occurs.
getTable
Future<Table?> getTable(int id, [
List<({String table, String column})> dropKeys = const [],
])
Acquire table class instance by id.
If not exist, returns null.
deleteTable
Future<int> deleteTable({String? where, List<Object?>? whereArgs})
Delete records from table.
Returns number of records deleted.
deleteTableByIds
Future<int> deleteTableByIds(List<Table> itemsList)
Delete records by instance of table class.
Returns number of records deleted.
_$AppDatabase Database Methods
schemaVersion
int get schemaVersion
Acquire schema version specified in @KegDatabase annotation.
getPathToOpen
Future<String> getPathToOpen()
Acquire path of SQLite file.
If you want to use default directory, return value can be relative path.
open
Future<void> open()
Open or create SQLite file.
openInMemory
Future<void> openInMemory()
Open or create SQLite file in memory.
close
Future<void> close()
Close SQLite file.
onConfigure
Future<void> onConfigure(Database db)
Call back function on configure database. Called on open.
onCreate
Future<void> onCreate(Database db, int version)
Call back function on create database.
onUpgrade
Future<void> onUpgrade(Database db, int oldVersion, int newVersion)
Call back function on upgrade database.
onDowngrade
Future<void> onDowngrade(Database db, int oldVersion, int newVersion)
Call back function on downgrade database.
keg_generator does not support downgrade, so UnimplementedError occurs
if downgrade happens.
onOpen
Future<void> onOpen(Database db)
Call back function on open database.
_$AppDatabase Other methods
These are mainly pass through methods to sqflite Database class.
transaction
Future<T> transaction<T>(
Future<T> Function(_$AppDatabaseTransactionWrapper txn) action,
{bool? exclusive,}
)
Start transaction.
readTransaction
Future<T> readTransaction<T>(
Future<T> Function(_$AppDatabaseTransactionWrapper txn) action,
)
Start read transaction.
batch
_$AppDatabaseBatchWrapper batch()
Start batch.
path
String get path
Actual full path of SQLite file.
isOpen
bool get isOpen
Database is open or not.
execute
Future<void> execute(String sql, [List<Object?>? arguments])
Execute SQLite statement.
rawInsert
Future<int> rawInsert(String sql, [List<Object?>? arguments])
Insert.
insert
Future<int> insert(
String table,
Map<String, Object?> values, {
String? nullColumnHack,
ConflictAlgorithm? conflictAlgorithm,
})
Insert.
query
Future<List<Map<String, Object?>>> query(
String table, {
bool? distinct,
List<String>? columns,
String? where,
List<Object?>? whereArgs,
String? groupBy,
String? having,
String? orderBy,
int? limit,
int? offset,
})
Query records and return list of map.
rawQuery
Future<List<Map<String, Object?>>> rawQuery(
String sql,
[List<Object?>? arguments,]
)
Query records and return list of map.
queryCursor
Future<QueryCursor> queryCursor(
String table, {
bool? distinct,
List<String>? columns,
String? where,
List<Object?>? whereArgs,
String? groupBy,
String? having,
String? orderBy,
int? limit,
int? offset,
int? bufferSize,
})
Query records and return list of map.
rawQueryCursor
Future<QueryCursor> rawQueryCursor(
String sql,
List<Object?>? arguments,
{int? bufferSize, }
)
Query records and return list of map.
update
Future<int> update(
String table,
Map<String, Object?> values, {
String? where,
List<Object?>? whereArgs,
ConflictAlgorithm? conflictAlgorithm,
})
Update.
rawUpdate
Future<int> rawUpdate(String sql, [List<Object?>? arguments])
Update.
delete
Future<int> delete(String table, {String? where, List<Object?>? whereArgs})
Delete.
rawDelete
Future<int> rawDelete(String sql, [List<Object?>? arguments])
Delete.
_$TableHelper
Table helper class is generated per each table class.
class _$TableHelper
_$TableHelper Fields
tableName
final String tableName
table name
final String tableName = '"user"';
column
final column
Dart record type which holds name of columns of SQLite table.
final column = (id: '"id"', name: '"name"');
columnTypes
final Map<String, String> columnTypes
Column data types.
Key is name of column, and value is SQLite data type.
final columnTypes = {
'id': 'INTEGER PRIMARY KEY AUTOINCREMENT',
'name': 'TEXT NOT NULL DEFAULT \'\'',
};
columnList
final List<String> columnList
List of column names.
final columnList = ['id', 'name'];
vnColumnList
static final List<String> v1ColumnList
List of column names by each version.(v1ColumnList, v2ColumnList...)
static final v1ColumnList = ['id', 'name'];
columnListByVersion
final Map<int, List<String>> columnListByVersion
Map of column list by each version
final columnListByVersion = {1: v1ColumnList, 2: v2ColumnList};
appdb
_$AppDatabase appdb;
Link to AppDatabase
_$TableHelper CRUD Methods
register
Future<int> register(Table item, _$AppDatabaseExecutor db)
Actual implementation of AppDatabase registerTable.
registerBatch
void registerBatch(Table item, _$AppDatabaseBatchWrapper batch)
Actual implementation of AppDatabase registerTable on batch.
query
Future<List<Table>> query(
_$AppDatabaseExecutor db, {
String? where,
List<Object?>? whereArgs,
String? orderBy,
int? limit,
int? offset,
List<({String table, String column})> dropKeys = const [],
})
Actual implementation of AppDatabase queryTable.
queryBatch
void queryBatch(
_$AppDatabaseBatchWrapper batch, {
String? where,
List<Object?>? whereArgs,
String? orderBy,
int? limit,
int? offset,
List<({String table, String column})> dropKeys = const [],
})
Actual implementation of AppDatabase queryTable on batch.
get
Future<Table?> get(
int id,
_$AppDatabaseExecutor db, [
List<({String table, String column})> dropKeys = const [],
])
Actual implementation of AppDatabase getTable.
getBatch
void getBatch(
int id,
_$AppDatabaseBatchWrapper batch, [
List<({String table, String column})> dropKeys = const [],
])
Actual implementation of AppDatabase getTable on batch.
delete
Future<int> delete(
_$AppDatabaseExecutor db, {
String? where,
List<Object?>? whereArgs,
})
Actual implementation of AppDatabase deleteTable.
deleteBatch
Future<void> deleteBatch(
_$AppDatabaseBatchWrapper batch,
{String? where,
List<Object?>? whereArgs,
})
Actual implementation of AppDatabase deleteTable on batch.
deleteByIds
Future<int> deleteByIds(_$AppDatabaseExecutor db, List<Table> itemList)
Actual implementation of AppDatabase deleteTableByIds.
deleteByIdsBatch
void deleteByIdsBatch(_$AppDatabaseBatchWrapper batch, List<Table> itemList)
Actual implementation of AppDatabase deleteTableByIds on batch.
_$TableHelper Database Methods
onCreate
Future<void> onCreate(
int version, {
DatabaseExecutor? db,
Batch? batch,})
Call back function on create table.
onUpgrade
Future<void> onUpgrade(
int oldVersion,
int newVersion, {
DatabaseExecutor? db,
Batch? batch,
})
Call back function on upgrade table.
_$TableHelper Map Methods
toSqlMap
static Map<String, Object?> toSqlMap(Table item)
Convert table class instance to Map.
fromSqlMap
static Table fromSqlMap(Map<String, Object?> map)
Convert Map to table class instance.
_$TableHelper Other Methods
convertReferences
Future<List<Map<String, Object?>>> convertReferences(
List<Map<String, Object?>> mapList,
_$AppDatabaseExecutor db,
List<({String table, String column})> dropKeys,
)
Called from query method.
If this table is linked to other, additional query is executed here.
About dropKeys see query description in _$AppDatabase.
mapToObject
List<User> mapToObject(List<Map<String, Object?>> mapList)
Called from query method.
Finally covert from Map to table class instance including linked tables.
compareManyToMany
bool compareManyToMany(Table item1, Set<int> set2)
Called from register method if many to many relation exists.
Compare if list of ids are equal or not.
registerManyToMany
Future<void> registerManyToMany(
Order item,
_$AppDatabaseExecutor executor,
)
Called from register method if many to many relation exists.
Register records of in the middle table.
_$AppDatabaseExecutor
abstract class _$AppDatabaseExecutor extends DatabaseExecutor
Interface class for _$AppDatabase and transaction. Extends sqflite DatabaseExecutor
_$AppDatabaseTransactionWrapper
class _$AppDatabaseTransactionWrapper implements _$AppDatabaseExecutor
Wrapper class for sqflite Transaction
_$AppDatabaseBatchWrapper
class _$AppDatabaseBatchWrapper implements Batch
Wrapper class for sqflite Batch
Additional information
If you need to see full code of these samples, check keg_tenerator test codes.
If you have any problems and questions, feel free to post issues to github.
Pull requests are also welcome.
Libraries
- keg_generator
- Support for doing something awesome.