mirror of https://github.com/AMT-Cheif/drift.git
Initial support for generated columns
This commit is contained in:
parent
5f8b74c8df
commit
3469bade62
|
@ -1,12 +1,13 @@
|
|||
## 1.1.0-dev
|
||||
|
||||
- Add the `references` method to `BuildColumn` to reference a column declared
|
||||
- Add the `references` method to `BuildColumn` to reference a column declared
|
||||
in another Dart table.
|
||||
- Add the `generateInsertable` option to `@UseRowClass`. When enabled, the generator
|
||||
will emit an extension to use the row class as an `Insertable`.
|
||||
Thanks to [@westito](https://github.com/westito).
|
||||
- Allow the generator to emit correct SQL code when using arrays with the
|
||||
`new_sql_code_generation` option in specific scenarios.
|
||||
- Add the `generatedAs` method to declare generated columns for Dart tables.
|
||||
- Improved support for pausing query stream subscriptions. Instead of buffering events,
|
||||
query streams will suspend fetching data if all listeners are paused.
|
||||
|
||||
|
|
|
@ -62,32 +62,22 @@ typedef BlobColumn = Column<Uint8List?>;
|
|||
/// A column that stores floating point numeric values.
|
||||
typedef RealColumn = Column<double?>;
|
||||
|
||||
class _BaseColumnBuilder<T> {}
|
||||
|
||||
/// A column builder is used to specify which columns should appear in a table.
|
||||
/// All of the methods defined in this class and its subclasses are not meant to
|
||||
/// be called at runtime. Instead, the generator will take a look at your
|
||||
/// source code (specifically, it will analyze which of the methods you use) to
|
||||
/// figure out the column structure of a table.
|
||||
class ColumnBuilder<T> {}
|
||||
class ColumnBuilder<T> extends _BaseColumnBuilder<T> {}
|
||||
|
||||
/// A column builder for virtual, generated columns.
|
||||
///
|
||||
/// This is a different class so that some methods are not available
|
||||
class VirtualColumnBuilder<T> extends _BaseColumnBuilder<T> {}
|
||||
|
||||
/// DSL extension to define a column inside a drift table.
|
||||
extension BuildColumn<T> on ColumnBuilder<T> {
|
||||
/// By default, the field name will be used as the column name, e.g.
|
||||
/// `IntColumn get id = integer()` will have "id" as its associated name.
|
||||
/// Columns made up of multiple words are expected to be in camelCase and will
|
||||
/// be converted to snake_case (e.g. a getter called accountCreationDate will
|
||||
/// result in an SQL column called account_creation_date).
|
||||
/// To change this default behavior, use something like
|
||||
/// `IntColumn get id = integer((c) => c.named('user_id'))`.
|
||||
///
|
||||
/// Note that using [named] __does not__ have an effect on the json key of an
|
||||
/// object. To change the json key, annotate this column getter with
|
||||
/// [JsonKey].
|
||||
ColumnBuilder<T> named(String name) => _isGenerated();
|
||||
|
||||
/// Marks this column as nullable. Nullable columns should not appear in a
|
||||
/// primary key. Columns are non-null by default.
|
||||
ColumnBuilder<T?> nullable() => _isGenerated();
|
||||
|
||||
/// Tells drift to write a custom constraint after this column definition when
|
||||
/// writing this column, for instance in a CREATE TABLE statement.
|
||||
///
|
||||
|
@ -164,45 +154,6 @@ extension BuildColumn<T> on ColumnBuilder<T> {
|
|||
/// apply the default value.
|
||||
ColumnBuilder<T> clientDefault(T Function() onInsert) => _isGenerated();
|
||||
|
||||
/// Uses a custom [converter] to store custom Dart objects in a single column
|
||||
/// and automatically mapping them from and to sql.
|
||||
///
|
||||
/// An example might look like this:
|
||||
/// ```dart
|
||||
/// // this is the custom object with we want to store in a column. It
|
||||
/// // can be as complex as you want it to be
|
||||
/// class MyCustomObject {
|
||||
/// final String data;
|
||||
/// MyCustomObject(this.data);
|
||||
/// }
|
||||
///
|
||||
/// class CustomConverter extends TypeConverter<MyCustomObject, String> {
|
||||
/// // this class is responsible for turning a custom object into a string.
|
||||
/// // this is easy here, but more complex objects could be serialized using
|
||||
/// // json or any other method of your choice.
|
||||
/// const CustomConverter();
|
||||
/// @override
|
||||
/// MyCustomObject mapToDart(String fromDb) {
|
||||
/// return fromDb == null ? null : MyCustomObject(fromDb);
|
||||
/// }
|
||||
///
|
||||
/// @override
|
||||
/// String mapToSql(MyCustomObject value) {
|
||||
/// return value?.data;
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// ```
|
||||
///
|
||||
/// In that case, you could have a table with this column
|
||||
/// ```dart
|
||||
/// TextColumn get custom => text().map(const CustomConverter())();
|
||||
/// ```
|
||||
/// The generated row class will then use a `MyFancyClass` instead of a
|
||||
/// `String`, which would usually be used for [Table.text] columns.
|
||||
ColumnBuilder<T> map<Dart>(TypeConverter<Dart, T> converter) =>
|
||||
_isGenerated();
|
||||
|
||||
/// Adds a foreign-key reference from this column.
|
||||
///
|
||||
/// The [table] type must be a Dart class name defining a drift table.
|
||||
|
@ -242,6 +193,104 @@ extension BuildColumn<T> on ColumnBuilder<T> {
|
|||
_isGenerated();
|
||||
}
|
||||
|
||||
/// Declare a generated column.
|
||||
///
|
||||
/// Generated columns are backed by an expression, declared with
|
||||
/// [generatedAs]:
|
||||
///
|
||||
/// ```dart
|
||||
/// class Products extends Table {
|
||||
/// TextColumn get name => text()();
|
||||
///
|
||||
/// RealColumn get price => real()();
|
||||
/// RealColumn get discount => real()();
|
||||
/// RealColumn get tax => real()();
|
||||
/// RealColumn get netPrice => real().generatedAs(
|
||||
/// price * (Constant(1) - discount) * (Constant(1) + tax))();
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Generated columns can either be `VIRTUAL` (the default) or `STORED`
|
||||
/// (enabled with the [stored] parameter). Stored generated columns are
|
||||
/// computed on each update and are stored in the database. Virtual columns
|
||||
/// consume less space, but are re-computed on each read.
|
||||
///
|
||||
/// Generated columns can't be updated or inserted (neither with the Dart API
|
||||
/// or though SQL queries), so they are not represented in companions.
|
||||
///
|
||||
/// __Important__: When a generated column can be nullable, don't forget to
|
||||
/// call [BuildGeneralColumn.nullable] on it to reflect this in the generated
|
||||
/// code.
|
||||
/// Also, note that generated columns are part of your databases schema and
|
||||
/// cannot be updated easily. When changing the [generatedAs] expression for a
|
||||
/// column, you need to re-generate the table with a [TableMigration].
|
||||
///
|
||||
/// Note that generated columns are only available in sqlite3 version
|
||||
/// `3.31.0`. When using `sqlite3_flutter_libs` or a web database, this is not
|
||||
/// a problem.
|
||||
VirtualColumnBuilder<T> generatedAs(Expression<T?> generatedAs,
|
||||
{bool stored = false}) =>
|
||||
_isGenerated();
|
||||
}
|
||||
|
||||
/// Column builders available for both virtual and non-virtual columns.
|
||||
extension BuildGeneralColumn<T> on _BaseColumnBuilder<T> {
|
||||
/// By default, the field name will be used as the column name, e.g.
|
||||
/// `IntColumn get id = integer()` will have "id" as its associated name.
|
||||
/// Columns made up of multiple words are expected to be in camelCase and will
|
||||
/// be converted to snake_case (e.g. a getter called accountCreationDate will
|
||||
/// result in an SQL column called account_creation_date).
|
||||
/// To change this default behavior, use something like
|
||||
/// `IntColumn get id = integer((c) => c.named('user_id'))`.
|
||||
///
|
||||
/// Note that using [named] __does not__ have an effect on the json key of an
|
||||
/// object. To change the json key, annotate this column getter with
|
||||
/// [JsonKey].
|
||||
ColumnBuilder<T> named(String name) => _isGenerated();
|
||||
|
||||
/// Marks this column as nullable. Nullable columns should not appear in a
|
||||
/// primary key. Columns are non-null by default.
|
||||
ColumnBuilder<T?> nullable() => _isGenerated();
|
||||
|
||||
/// Uses a custom [converter] to store custom Dart objects in a single column
|
||||
/// and automatically mapping them from and to sql.
|
||||
///
|
||||
/// An example might look like this:
|
||||
/// ```dart
|
||||
/// // this is the custom object with we want to store in a column. It
|
||||
/// // can be as complex as you want it to be
|
||||
/// class MyCustomObject {
|
||||
/// final String data;
|
||||
/// MyCustomObject(this.data);
|
||||
/// }
|
||||
///
|
||||
/// class CustomConverter extends TypeConverter<MyCustomObject, String> {
|
||||
/// // this class is responsible for turning a custom object into a string.
|
||||
/// // this is easy here, but more complex objects could be serialized using
|
||||
/// // json or any other method of your choice.
|
||||
/// const CustomConverter();
|
||||
/// @override
|
||||
/// MyCustomObject mapToDart(String fromDb) {
|
||||
/// return fromDb == null ? null : MyCustomObject(fromDb);
|
||||
/// }
|
||||
///
|
||||
/// @override
|
||||
/// String mapToSql(MyCustomObject value) {
|
||||
/// return value?.data;
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// ```
|
||||
///
|
||||
/// In that case, you could have a table with this column
|
||||
/// ```dart
|
||||
/// TextColumn get custom => text().map(const CustomConverter())();
|
||||
/// ```
|
||||
/// The generated row class will then use a `MyFancyClass` instead of a
|
||||
/// `String`, which would usually be used for [Table.text] columns.
|
||||
ColumnBuilder<T> map<Dart>(TypeConverter<Dart, T> converter) =>
|
||||
_isGenerated();
|
||||
|
||||
/// Turns this column builder into a column. This method won't actually be
|
||||
/// called in your code. Instead, the generator will take a look at your
|
||||
/// source code to figure out your table structure.
|
||||
|
|
|
@ -44,6 +44,10 @@ class GeneratedColumn<T> extends Column<T> {
|
|||
/// The sql type name, such as TEXT for texts.
|
||||
final String typeName;
|
||||
|
||||
/// If this column is generated (that is, it is a SQL expression of other)
|
||||
/// columns, contains information about how to generate this column.
|
||||
final GeneratedAs? generatedAs;
|
||||
|
||||
/// Whether a value is required for this column when inserting a new row.
|
||||
final bool requiredDuringInsert;
|
||||
|
||||
|
@ -67,6 +71,7 @@ class GeneratedColumn<T> extends Column<T> {
|
|||
this.defaultValue,
|
||||
this.additionalChecks,
|
||||
this.requiredDuringInsert = false,
|
||||
this.generatedAs,
|
||||
}) : _defaultConstraints = defaultConstraints;
|
||||
|
||||
/// Applies a type converter to this column.
|
||||
|
@ -86,6 +91,7 @@ class GeneratedColumn<T> extends Column<T> {
|
|||
defaultValue,
|
||||
additionalChecks,
|
||||
requiredDuringInsert,
|
||||
generatedAs,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -111,6 +117,15 @@ class GeneratedColumn<T> extends Column<T> {
|
|||
if (writeBrackets) into.buffer.write(')');
|
||||
}
|
||||
|
||||
final generated = generatedAs;
|
||||
if (generated != null) {
|
||||
into.buffer.write(' GENERATED ALWAYS AS (');
|
||||
generated.generatedAs.writeInto(into);
|
||||
into.buffer
|
||||
..write(') ')
|
||||
..write(generated.stored ? 'STORED' : 'VIRTUAL');
|
||||
}
|
||||
|
||||
// these custom constraints refer to builtin constraints from drift
|
||||
if (_defaultConstraints != null) {
|
||||
into.buffer
|
||||
|
@ -221,6 +236,7 @@ class GeneratedColumnWithTypeConverter<D, S> extends GeneratedColumn<S> {
|
|||
Expression<S>? defaultValue,
|
||||
VerificationResult Function(S, VerificationMeta)? additionalChecks,
|
||||
bool requiredDuringInsert,
|
||||
GeneratedAs? generatedAs,
|
||||
) : super(
|
||||
name,
|
||||
tableName,
|
||||
|
@ -232,6 +248,7 @@ class GeneratedColumnWithTypeConverter<D, S> extends GeneratedColumn<S> {
|
|||
defaultValue: defaultValue,
|
||||
additionalChecks: additionalChecks,
|
||||
requiredDuringInsert: requiredDuringInsert,
|
||||
generatedAs: generatedAs,
|
||||
);
|
||||
|
||||
/// Compares this column against the mapped [dartValue].
|
||||
|
@ -241,3 +258,17 @@ class GeneratedColumnWithTypeConverter<D, S> extends GeneratedColumn<S> {
|
|||
return equals(converter.mapToSql(dartValue) as S);
|
||||
}
|
||||
}
|
||||
|
||||
/// Information filled out by the generator to support generated or virtual
|
||||
/// columns.
|
||||
class GeneratedAs {
|
||||
/// The expression that this column evaluates to.
|
||||
final Expression generatedAs;
|
||||
|
||||
/// Wehter this column is stored in the database, as opposed to being
|
||||
/// `VIRTUAL` and evaluated on each read.
|
||||
final bool stored;
|
||||
|
||||
/// Creates a [GeneratedAs] clause.
|
||||
GeneratedAs(this.generatedAs, this.stored);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
name: drift
|
||||
description: Drift is a reactive library to store relational data in Dart and Flutter applications.
|
||||
version: 1.0.0
|
||||
version: 1.1.0-dev
|
||||
repository: https://github.com/simolus3/moor
|
||||
homepage: https://drift.simonbinder.eu/
|
||||
issue_tracker: https://github.com/simolus3/moor/issues
|
||||
|
|
|
@ -38,6 +38,9 @@ class Categories extends Table with AutoIncrement {
|
|||
text().named('desc').customConstraint('NOT NULL UNIQUE')();
|
||||
IntColumn get priority =>
|
||||
intEnum<CategoryPriority>().withDefault(const Constant(0))();
|
||||
|
||||
TextColumn get descriptionInUpperCase =>
|
||||
text().generatedAs(description.upper())();
|
||||
}
|
||||
|
||||
enum CategoryPriority { low, medium, high }
|
||||
|
|
|
@ -11,8 +11,12 @@ class Category extends DataClass implements Insertable<Category> {
|
|||
final int id;
|
||||
final String description;
|
||||
final CategoryPriority priority;
|
||||
final String descriptionInUpperCase;
|
||||
Category(
|
||||
{required this.id, required this.description, required this.priority});
|
||||
{required this.id,
|
||||
required this.description,
|
||||
required this.priority,
|
||||
required this.descriptionInUpperCase});
|
||||
factory Category.fromData(Map<String, dynamic> data, {String? prefix}) {
|
||||
final effectivePrefix = prefix ?? '';
|
||||
return Category(
|
||||
|
@ -22,6 +26,8 @@ class Category extends DataClass implements Insertable<Category> {
|
|||
.mapFromDatabaseResponse(data['${effectivePrefix}desc'])!,
|
||||
priority: $CategoriesTable.$converter0.mapToDart(const IntType()
|
||||
.mapFromDatabaseResponse(data['${effectivePrefix}priority']))!,
|
||||
descriptionInUpperCase: const StringType().mapFromDatabaseResponse(
|
||||
data['${effectivePrefix}description_in_upper_case'])!,
|
||||
);
|
||||
}
|
||||
@override
|
||||
|
@ -51,6 +57,8 @@ class Category extends DataClass implements Insertable<Category> {
|
|||
id: serializer.fromJson<int>(json['id']),
|
||||
description: serializer.fromJson<String>(json['description']),
|
||||
priority: serializer.fromJson<CategoryPriority>(json['priority']),
|
||||
descriptionInUpperCase:
|
||||
serializer.fromJson<String>(json['descriptionInUpperCase']),
|
||||
);
|
||||
}
|
||||
factory Category.fromJsonString(String encodedJson,
|
||||
|
@ -65,35 +73,45 @@ class Category extends DataClass implements Insertable<Category> {
|
|||
'id': serializer.toJson<int>(id),
|
||||
'description': serializer.toJson<String>(description),
|
||||
'priority': serializer.toJson<CategoryPriority>(priority),
|
||||
'descriptionInUpperCase':
|
||||
serializer.toJson<String>(descriptionInUpperCase),
|
||||
};
|
||||
}
|
||||
|
||||
Category copyWith(
|
||||
{int? id, String? description, CategoryPriority? priority}) =>
|
||||
{int? id,
|
||||
String? description,
|
||||
CategoryPriority? priority,
|
||||
String? descriptionInUpperCase}) =>
|
||||
Category(
|
||||
id: id ?? this.id,
|
||||
description: description ?? this.description,
|
||||
priority: priority ?? this.priority,
|
||||
descriptionInUpperCase:
|
||||
descriptionInUpperCase ?? this.descriptionInUpperCase,
|
||||
);
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('Category(')
|
||||
..write('id: $id, ')
|
||||
..write('description: $description, ')
|
||||
..write('priority: $priority')
|
||||
..write('priority: $priority, ')
|
||||
..write('descriptionInUpperCase: $descriptionInUpperCase')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(id, description, priority);
|
||||
int get hashCode =>
|
||||
Object.hash(id, description, priority, descriptionInUpperCase);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is Category &&
|
||||
other.id == this.id &&
|
||||
other.description == this.description &&
|
||||
other.priority == this.priority);
|
||||
other.priority == this.priority &&
|
||||
other.descriptionInUpperCase == this.descriptionInUpperCase);
|
||||
}
|
||||
|
||||
class CategoriesCompanion extends UpdateCompanion<Category> {
|
||||
|
@ -185,8 +203,16 @@ class $CategoriesTable extends Categories
|
|||
requiredDuringInsert: false,
|
||||
defaultValue: const Constant(0))
|
||||
.withConverter<CategoryPriority>($CategoriesTable.$converter0);
|
||||
final VerificationMeta _descriptionInUpperCaseMeta =
|
||||
const VerificationMeta('descriptionInUpperCase');
|
||||
late final GeneratedColumn<String?> descriptionInUpperCase =
|
||||
GeneratedColumn<String?>('description_in_upper_case', aliasedName, false,
|
||||
typeName: 'TEXT',
|
||||
requiredDuringInsert: false,
|
||||
generatedAs: GeneratedAs(description.upper(), false));
|
||||
@override
|
||||
List<GeneratedColumn> get $columns => [id, description, priority];
|
||||
List<GeneratedColumn> get $columns =>
|
||||
[id, description, priority, descriptionInUpperCase];
|
||||
@override
|
||||
String get aliasedName => _alias ?? 'categories';
|
||||
@override
|
||||
|
@ -206,6 +232,12 @@ class $CategoriesTable extends Categories
|
|||
context.missing(_descriptionMeta);
|
||||
}
|
||||
context.handle(_priorityMeta, const VerificationResult.success());
|
||||
if (data.containsKey('description_in_upper_case')) {
|
||||
context.handle(
|
||||
_descriptionInUpperCaseMeta,
|
||||
descriptionInUpperCase.isAcceptableOrUnknown(
|
||||
data['description_in_upper_case']!, _descriptionInUpperCaseMeta));
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
|
|
|
@ -81,6 +81,7 @@ void main() {
|
|||
id: 3,
|
||||
description: 'description',
|
||||
priority: CategoryPriority.low,
|
||||
descriptionInUpperCase: 'ignored',
|
||||
);
|
||||
final companion = entry.toCompanion(false);
|
||||
|
||||
|
|
|
@ -386,6 +386,7 @@ void main() {
|
|||
{
|
||||
'id': 1,
|
||||
'desc': 'description',
|
||||
'description_in_upper_case': 'DESCRIPTION',
|
||||
'priority': 1,
|
||||
},
|
||||
]),
|
||||
|
|
|
@ -107,6 +107,7 @@ void main() {
|
|||
id: 1,
|
||||
description: 'Description',
|
||||
priority: CategoryPriority.low,
|
||||
descriptionInUpperCase: 'DESCRIPTION',
|
||||
),
|
||||
);
|
||||
}, skip: ifOlderThanSqlite335());
|
||||
|
|
|
@ -26,7 +26,8 @@ void main() {
|
|||
'SELECT t.id AS "t.id", t.title AS "t.title", '
|
||||
't.content AS "t.content", t.target_date AS "t.target_date", '
|
||||
't.category AS "t.category", c.id AS "c.id", c."desc" AS "c.desc", '
|
||||
'c.priority AS "c.priority" '
|
||||
'c.priority AS "c.priority", '
|
||||
'c.description_in_upper_case AS "c.description_in_upper_case" '
|
||||
'FROM todos t LEFT OUTER JOIN categories c ON c.id = t.category;',
|
||||
argThat(isEmpty)));
|
||||
});
|
||||
|
@ -46,6 +47,7 @@ void main() {
|
|||
't.category': 3,
|
||||
'c.id': 3,
|
||||
'c.desc': 'description',
|
||||
'c.description_in_upper_case': 'DESCRIPTION',
|
||||
'c.priority': 2,
|
||||
}
|
||||
]);
|
||||
|
@ -74,6 +76,7 @@ void main() {
|
|||
id: 3,
|
||||
description: 'description',
|
||||
priority: CategoryPriority.high,
|
||||
descriptionInUpperCase: 'DESCRIPTION',
|
||||
),
|
||||
);
|
||||
|
||||
|
@ -198,15 +201,22 @@ void main() {
|
|||
|
||||
when(executor.runSelect(any, any)).thenAnswer((_) async {
|
||||
return [
|
||||
{'c.id': 3, 'c.desc': 'Description', 'c.priority': 1, 'c3': 11}
|
||||
{
|
||||
'c.id': 3,
|
||||
'c.desc': 'Description',
|
||||
'c.description_in_upper_case': 'DESCRIPTION',
|
||||
'c.priority': 1,
|
||||
'c4': 11
|
||||
}
|
||||
];
|
||||
});
|
||||
|
||||
final result = await query.getSingle();
|
||||
|
||||
verify(executor.runSelect(
|
||||
'SELECT c.id AS "c.id", c."desc" AS "c.desc", c.priority AS "c.priority"'
|
||||
', LENGTH(c."desc") AS "c3" '
|
||||
'SELECT c.id AS "c.id", c."desc" AS "c.desc", '
|
||||
'c.priority AS "c.priority", c.description_in_upper_case AS '
|
||||
'"c.description_in_upper_case", LENGTH(c."desc") AS "c4" '
|
||||
'FROM categories c;',
|
||||
[],
|
||||
));
|
||||
|
@ -217,6 +227,7 @@ void main() {
|
|||
Category(
|
||||
id: 3,
|
||||
description: 'Description',
|
||||
descriptionInUpperCase: 'DESCRIPTION',
|
||||
priority: CategoryPriority.medium,
|
||||
),
|
||||
),
|
||||
|
@ -239,7 +250,13 @@ void main() {
|
|||
|
||||
when(executor.runSelect(any, any)).thenAnswer((_) async {
|
||||
return [
|
||||
{'c.id': 3, 'c.desc': 'Description', 'c.priority': 1, 'c3': 11}
|
||||
{
|
||||
'c.id': 3,
|
||||
'c.desc': 'Description',
|
||||
'c.description_in_upper_case': 'DESCRIPTION',
|
||||
'c.priority': 1,
|
||||
'c4': 11,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
|
@ -247,7 +264,8 @@ void main() {
|
|||
|
||||
verify(executor.runSelect(
|
||||
'SELECT c.id AS "c.id", c."desc" AS "c.desc", c.priority AS "c.priority"'
|
||||
', LENGTH(c."desc") AS "c3" '
|
||||
', c.description_in_upper_case AS "c.description_in_upper_case", '
|
||||
'LENGTH(c."desc") AS "c4" '
|
||||
'FROM categories c '
|
||||
'INNER JOIN todos t ON c.id = t.category;',
|
||||
[],
|
||||
|
@ -259,6 +277,7 @@ void main() {
|
|||
Category(
|
||||
id: 3,
|
||||
description: 'Description',
|
||||
descriptionInUpperCase: 'DESCRIPTION',
|
||||
priority: CategoryPriority.medium,
|
||||
),
|
||||
),
|
||||
|
@ -287,7 +306,13 @@ void main() {
|
|||
|
||||
when(executor.runSelect(any, any)).thenAnswer((_) async {
|
||||
return [
|
||||
{'c.id': 3, 'c.desc': 'desc', 'c.priority': 0, 'c3': 10}
|
||||
{
|
||||
'c.id': 3,
|
||||
'c.desc': 'desc',
|
||||
'c.priority': 0,
|
||||
'c4': 10,
|
||||
'c.description_in_upper_case': 'DESC',
|
||||
}
|
||||
];
|
||||
});
|
||||
|
||||
|
@ -295,7 +320,9 @@ void main() {
|
|||
|
||||
verify(executor.runSelect(
|
||||
'SELECT c.id AS "c.id", c."desc" AS "c.desc", '
|
||||
'c.priority AS "c.priority", COUNT(t.id) AS "c3" '
|
||||
'c.priority AS "c.priority", '
|
||||
'c.description_in_upper_case AS "c.description_in_upper_case", '
|
||||
'COUNT(t.id) AS "c4" '
|
||||
'FROM categories c INNER JOIN todos t ON t.category = c.id '
|
||||
'GROUP BY c.id HAVING COUNT(t.id) >= ?;',
|
||||
[10]));
|
||||
|
@ -306,6 +333,7 @@ void main() {
|
|||
Category(
|
||||
id: 3,
|
||||
description: 'desc',
|
||||
descriptionInUpperCase: 'DESC',
|
||||
priority: CategoryPriority.low,
|
||||
),
|
||||
);
|
||||
|
|
|
@ -30,7 +30,10 @@ void main() {
|
|||
'CREATE TABLE IF NOT EXISTS categories '
|
||||
'(id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, '
|
||||
'"desc" TEXT NOT NULL UNIQUE, '
|
||||
'priority INTEGER NOT NULL DEFAULT 0);',
|
||||
'priority INTEGER NOT NULL DEFAULT 0, '
|
||||
'description_in_upper_case TEXT NOT NULL GENERATED ALWAYS AS '
|
||||
'(UPPER("desc")) VIRTUAL'
|
||||
');',
|
||||
[]));
|
||||
|
||||
verify(mockExecutor.runCustom(
|
||||
|
|
|
@ -175,6 +175,7 @@ void main() {
|
|||
{
|
||||
'id': 1,
|
||||
'desc': 'description',
|
||||
'description_in_upper_case': 'DESCRIPTION',
|
||||
'priority': 2,
|
||||
}
|
||||
]);
|
||||
|
@ -187,6 +188,7 @@ void main() {
|
|||
Category(
|
||||
id: 1,
|
||||
description: 'description',
|
||||
descriptionInUpperCase: 'DESCRIPTION',
|
||||
priority: CategoryPriority.high,
|
||||
),
|
||||
);
|
||||
|
|
|
@ -88,7 +88,12 @@ void main() {
|
|||
await first.first; // subscribe to first stream, then drop subscription
|
||||
|
||||
when(executor.runSelect(any, any)).thenAnswer((_) => Future.value([
|
||||
{'id': 1, 'desc': 'd', 'priority': 0}
|
||||
{
|
||||
'id': 1,
|
||||
'desc': 'd',
|
||||
'description_in_upper_case': 'D',
|
||||
'priority': 0,
|
||||
}
|
||||
]));
|
||||
await db
|
||||
.into(db.categories)
|
||||
|
@ -104,7 +109,12 @@ void main() {
|
|||
final subscription = first.listen((_) {});
|
||||
|
||||
when(executor.runSelect(any, any)).thenAnswer((_) => Future.value([
|
||||
{'id': 1, 'desc': 'd', 'priority': 0}
|
||||
{
|
||||
'id': 1,
|
||||
'desc': 'd',
|
||||
'description_in_upper_case': 'D',
|
||||
'priority': 0,
|
||||
}
|
||||
]));
|
||||
await db
|
||||
.into(db.categories)
|
||||
|
@ -246,7 +256,12 @@ void main() {
|
|||
|
||||
// Return a new row on the next select
|
||||
when(executor.runSelect(any, any)).thenAnswer((_) => Future.value([
|
||||
{'id': 1, 'desc': 'd', 'priority': 0}
|
||||
{
|
||||
'id': 1,
|
||||
'desc': 'd',
|
||||
'description_in_upper_case': 'D',
|
||||
'priority': 0,
|
||||
}
|
||||
]));
|
||||
db.markTablesUpdated([db.categories]);
|
||||
await pumpEventQueue();
|
||||
|
|
|
@ -35,20 +35,15 @@ void main() {
|
|||
});
|
||||
|
||||
test('can convert a companion to a row class', () {
|
||||
const companion = CategoriesCompanion(
|
||||
id: Value(3),
|
||||
description: Value('description'),
|
||||
priority: Value(CategoryPriority.low),
|
||||
const companion = SharedTodosCompanion(
|
||||
todo: Value(3),
|
||||
user: Value(4),
|
||||
);
|
||||
|
||||
final user = db.categories.mapFromCompanion(companion);
|
||||
final user = db.sharedTodos.mapFromCompanion(companion);
|
||||
expect(
|
||||
user,
|
||||
Category(
|
||||
id: 3,
|
||||
description: 'description',
|
||||
priority: CategoryPriority.low,
|
||||
),
|
||||
SharedTodo(todo: 3, user: 4),
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ const String _methodCustomConstraint = 'customConstraint';
|
|||
const String _methodDefault = 'withDefault';
|
||||
const String _methodClientDefault = 'clientDefault';
|
||||
const String _methodMap = 'map';
|
||||
const String _methodGenerated = 'generatedAs';
|
||||
|
||||
const String _errorMessage = 'This getter does not create a valid column that '
|
||||
'can be parsed by moor. Please refer to the readme from moor to see how '
|
||||
|
@ -70,6 +71,7 @@ class ColumnParser {
|
|||
Expression? clientDefaultExpression;
|
||||
Expression? createdTypeConverter;
|
||||
DartType? typeConverterRuntime;
|
||||
ColumnGeneratedAs? generatedAs;
|
||||
var nullable = false;
|
||||
|
||||
final foundFeatures = <ColumnFeature>[];
|
||||
|
@ -249,6 +251,32 @@ class ColumnParser {
|
|||
createdTypeConverter = expression;
|
||||
typeConverterRuntime = type;
|
||||
break;
|
||||
case _methodGenerated:
|
||||
Expression? generatedExpression;
|
||||
var stored = false;
|
||||
|
||||
for (final expr in remainingExpr.argumentList.arguments) {
|
||||
if (expr is NamedExpression && expr.name.label.name == 'stored') {
|
||||
final storedValue = expr.expression;
|
||||
if (storedValue is BooleanLiteral) {
|
||||
stored = storedValue.value;
|
||||
} else {
|
||||
base.step.reportError(ErrorInDartCode(
|
||||
message: 'Must be a boolean literal',
|
||||
affectedNode: expr,
|
||||
affectedElement: element,
|
||||
));
|
||||
}
|
||||
} else {
|
||||
generatedExpression = expr;
|
||||
}
|
||||
}
|
||||
|
||||
if (generatedExpression != null) {
|
||||
final code = element.source!.contents.data
|
||||
.substring(generatedExpression.offset, generatedExpression.end);
|
||||
generatedAs = ColumnGeneratedAs(code, stored);
|
||||
}
|
||||
}
|
||||
|
||||
// We're not at a starting method yet, so we need to go deeper!
|
||||
|
@ -322,10 +350,11 @@ class ColumnParser {
|
|||
typeConverter: converter,
|
||||
declaration: DartColumnDeclaration(element, base.step.file),
|
||||
documentationComment: docString,
|
||||
generatedAs: generatedAs,
|
||||
);
|
||||
}
|
||||
|
||||
ColumnType _startMethodToColumnType(String startMethod) {
|
||||
ColumnType _startMethodToColumnType(String name) {
|
||||
return const {
|
||||
startBool: ColumnType.boolean,
|
||||
startString: ColumnType.text,
|
||||
|
@ -334,7 +363,7 @@ class ColumnParser {
|
|||
startDateTime: ColumnType.datetime,
|
||||
startBlob: ColumnType.blob,
|
||||
startReal: ColumnType.real,
|
||||
}[startMethod]!;
|
||||
}[name]!;
|
||||
}
|
||||
|
||||
String? _readJsonKey(Element getter) {
|
||||
|
|
|
@ -55,6 +55,7 @@ class CreateTableReader {
|
|||
UsedTypeConverter? converter;
|
||||
String? defaultValue;
|
||||
String? overriddenJsonKey;
|
||||
ColumnGeneratedAs? generatedAs;
|
||||
|
||||
final typeName = column.definition?.typeName;
|
||||
|
||||
|
@ -158,6 +159,7 @@ class CreateTableReader {
|
|||
typeConverter: converter,
|
||||
overriddenJsonName: overriddenJsonKey,
|
||||
declaration: declaration,
|
||||
generatedAs: generatedAs,
|
||||
);
|
||||
|
||||
foundColumns[column.name] = parsed;
|
||||
|
|
|
@ -106,6 +106,10 @@ class MoorColumn implements HasDeclaration, HasType {
|
|||
/// Stored as a multi line string with leading triple-slashes `///` for every line
|
||||
final String? documentationComment;
|
||||
|
||||
final ColumnGeneratedAs? generatedAs;
|
||||
|
||||
bool get isGenerated => generatedAs != null;
|
||||
|
||||
/// The column type from the dsl library. For instance, if a table has
|
||||
/// declared an `IntColumn`, the matching dsl column name would also be an
|
||||
/// `IntColumn`.
|
||||
|
@ -180,6 +184,7 @@ class MoorColumn implements HasDeclaration, HasType {
|
|||
this.typeConverter,
|
||||
this.declaration,
|
||||
this.documentationComment,
|
||||
this.generatedAs,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -256,3 +261,10 @@ class ResolvedDartForeignKeyReference extends ColumnFeature {
|
|||
ResolvedDartForeignKeyReference(
|
||||
this.otherTable, this.otherColumn, this.onUpdate, this.onDelete);
|
||||
}
|
||||
|
||||
class ColumnGeneratedAs {
|
||||
final String? dartExpression;
|
||||
final bool stored;
|
||||
|
||||
ColumnGeneratedAs(this.dartExpression, this.stored);
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ class DartColumnDeclaration implements DartDeclaration, ColumnDeclaration {
|
|||
@override
|
||||
final SourceRange declaration;
|
||||
|
||||
/// In the Dart api, columns declared via getters.
|
||||
/// In the Dart api, columns are declared via getters.
|
||||
@override
|
||||
final Element element;
|
||||
|
||||
|
|
|
@ -169,7 +169,8 @@ class MoorTable extends MoorEntityWithResultSet {
|
|||
|
||||
if (column.defaultArgument != null ||
|
||||
column.clientDefaultCode != null ||
|
||||
column.nullable) {
|
||||
column.nullable ||
|
||||
column.isGenerated) {
|
||||
// default value would be applied, so it's not required for inserts
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -211,6 +211,9 @@ class DataClassWriter {
|
|||
..write('final map = <String, Expression> {};');
|
||||
|
||||
for (final column in table.columns) {
|
||||
// Generated column - cannot be used for inserts or updates
|
||||
if (column.isGenerated) continue;
|
||||
|
||||
// We include all columns that are not null. If nullToAbsent is false, we
|
||||
// also include null columns. When generating NNBD code, we can include
|
||||
// non-nullable columns without an additional null check.
|
||||
|
@ -266,6 +269,9 @@ class DataClassWriter {
|
|||
..write('(');
|
||||
|
||||
for (final column in table.columns) {
|
||||
// Generated columns are not parts of companions.
|
||||
if (column.isGenerated) continue;
|
||||
|
||||
final dartName = column.dartGetterName;
|
||||
_buffer
|
||||
..write(dartName)
|
||||
|
|
|
@ -97,6 +97,16 @@ abstract class TableOrViewWriter {
|
|||
additionalParams['clientDefault'] = column.clientDefaultCode!;
|
||||
}
|
||||
|
||||
if (column.generatedAs != null) {
|
||||
final generateAs = column.generatedAs!;
|
||||
final code = generateAs.dartExpression;
|
||||
|
||||
if (code != null) {
|
||||
additionalParams['generatedAs'] =
|
||||
'GeneratedAs($code, ${generateAs.stored})';
|
||||
}
|
||||
}
|
||||
|
||||
final innerType = column.innerColumnType(options);
|
||||
var type = 'GeneratedColumn<$innerType>';
|
||||
expressionBuffer
|
||||
|
|
|
@ -10,6 +10,11 @@ class UpdateCompanionWriter {
|
|||
|
||||
late StringBuffer _buffer;
|
||||
|
||||
late final List<MoorColumn> columns = [
|
||||
for (final column in table.columns)
|
||||
if (!column.isGenerated) column,
|
||||
];
|
||||
|
||||
UpdateCompanionWriter(this.table, this.scope) {
|
||||
_buffer = scope.leaf();
|
||||
}
|
||||
|
@ -36,7 +41,7 @@ class UpdateCompanionWriter {
|
|||
}
|
||||
|
||||
void _writeFields() {
|
||||
for (final column in table.columns) {
|
||||
for (final column in columns) {
|
||||
final modifier = scope.options.fieldModifier;
|
||||
final type = column.dartTypeCode(scope.generationOptions);
|
||||
_buffer.write('$modifier Value<$type> ${column.dartGetterName};\n');
|
||||
|
@ -49,7 +54,7 @@ class UpdateCompanionWriter {
|
|||
}
|
||||
_buffer.write('${table.getNameForCompanionClass(scope.options)}({');
|
||||
|
||||
for (final column in table.columns) {
|
||||
for (final column in columns) {
|
||||
_buffer.write('this.${column.dartGetterName} = const Value.absent(),');
|
||||
}
|
||||
|
||||
|
@ -73,7 +78,7 @@ class UpdateCompanionWriter {
|
|||
// @required String b}): a = Value(a), b = Value(b);
|
||||
// We don't need to use this. for the initializers, Dart figures that out.
|
||||
|
||||
for (final column in table.columns) {
|
||||
for (final column in columns) {
|
||||
final param = column.dartGetterName;
|
||||
|
||||
if (table.isColumnRequiredForInsert(column)) {
|
||||
|
@ -106,18 +111,17 @@ class UpdateCompanionWriter {
|
|||
void _writeCustomConstructor() {
|
||||
// Prefer a .custom constructor, unless there already is a field called
|
||||
// "custom", in which case we'll use createCustom
|
||||
final constructorName = table.columns
|
||||
.map((e) => e.dartGetterName)
|
||||
.any((name) => name == 'custom')
|
||||
? 'createCustom'
|
||||
: 'custom';
|
||||
final constructorName =
|
||||
columns.map((e) => e.dartGetterName).any((name) => name == 'custom')
|
||||
? 'createCustom'
|
||||
: 'custom';
|
||||
|
||||
final dartTypeName = table.dartTypeCode(scope.generationOptions);
|
||||
_buffer
|
||||
..write('static Insertable<$dartTypeName> $constructorName')
|
||||
..write('({');
|
||||
|
||||
for (final column in table.columns) {
|
||||
for (final column in columns) {
|
||||
// todo (breaking change): This should not consider type converters.
|
||||
final typeName = column.dartTypeCode(scope.generationOptions);
|
||||
final type = scope.nullableType('Expression<$typeName>');
|
||||
|
@ -128,7 +132,7 @@ class UpdateCompanionWriter {
|
|||
..write('}) {\n')
|
||||
..write('return RawValuesInsertable({');
|
||||
|
||||
for (final column in table.columns) {
|
||||
for (final column in columns) {
|
||||
_buffer
|
||||
..write('if (${column.dartGetterName} != null)')
|
||||
..write(asDartLiteral(column.name.name))
|
||||
|
@ -143,7 +147,7 @@ class UpdateCompanionWriter {
|
|||
..write(table.getNameForCompanionClass(scope.options))
|
||||
..write(' copyWith({');
|
||||
var first = true;
|
||||
for (final column in table.columns) {
|
||||
for (final column in columns) {
|
||||
if (!first) {
|
||||
_buffer.write(', ');
|
||||
}
|
||||
|
@ -157,7 +161,7 @@ class UpdateCompanionWriter {
|
|||
_buffer
|
||||
..write('}) {\n') //
|
||||
..write('return ${table.getNameForCompanionClass(scope.options)}(');
|
||||
for (final column in table.columns) {
|
||||
for (final column in columns) {
|
||||
final name = column.dartGetterName;
|
||||
_buffer.write('$name: $name ?? this.$name,');
|
||||
}
|
||||
|
@ -173,7 +177,7 @@ class UpdateCompanionWriter {
|
|||
|
||||
const locals = {'map', 'nullToAbsent', 'converter'};
|
||||
|
||||
for (final column in table.columns) {
|
||||
for (final column in columns) {
|
||||
final getterName = column.thisIfNeeded(locals);
|
||||
|
||||
_buffer.write('if ($getterName.present) {');
|
||||
|
@ -212,7 +216,7 @@ class UpdateCompanionWriter {
|
|||
void _writeToString() {
|
||||
overrideToString(
|
||||
table.getNameForCompanionClass(scope.options),
|
||||
[for (final column in table.columns) column.dartGetterName],
|
||||
[for (final column in columns) column.dartGetterName],
|
||||
_buffer,
|
||||
);
|
||||
}
|
||||
|
@ -237,7 +241,7 @@ class UpdateCompanionWriter {
|
|||
final column =
|
||||
table.columns.firstWhereOrNull((e) => e.dartGetterName == field.name);
|
||||
|
||||
if (column != null) {
|
||||
if (column != null && !column.isGenerated) {
|
||||
final dartName = column.dartGetterName;
|
||||
_buffer.write('$dartName: Value (_object.$dartName),\n');
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue