Initial support for generated columns

This commit is contained in:
Simon Binder 2021-11-07 21:20:12 +01:00
parent 5f8b74c8df
commit 3469bade62
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
22 changed files with 332 additions and 106 deletions

View File

@ -7,6 +7,7 @@
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.

View File

@ -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.

View File

@ -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);
}

View File

@ -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

View File

@ -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 }

View File

@ -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;
}

View File

@ -81,6 +81,7 @@ void main() {
id: 3,
description: 'description',
priority: CategoryPriority.low,
descriptionInUpperCase: 'ignored',
);
final companion = entry.toCompanion(false);

View File

@ -386,6 +386,7 @@ void main() {
{
'id': 1,
'desc': 'description',
'description_in_upper_case': 'DESCRIPTION',
'priority': 1,
},
]),

View File

@ -107,6 +107,7 @@ void main() {
id: 1,
description: 'Description',
priority: CategoryPriority.low,
descriptionInUpperCase: 'DESCRIPTION',
),
);
}, skip: ifOlderThanSqlite335());

View File

@ -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,
),
);

View File

@ -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(

View File

@ -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,
),
);

View File

@ -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();

View File

@ -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),
);
});

View File

@ -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) {

View File

@ -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;

View File

@ -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);
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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)

View File

@ -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

View File

@ -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,9 +111,8 @@ 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')
final constructorName =
columns.map((e) => e.dartGetterName).any((name) => name == 'custom')
? 'createCustom'
: 'custom';
@ -117,7 +121,7 @@ class UpdateCompanionWriter {
..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');
}