Implement custom column constraints

This commit is contained in:
Simon Binder 2019-03-14 19:11:26 +01:00
parent 2e96ef1d56
commit 81fe2e7feb
No known key found for this signature in database
GPG Key ID: B807FDF954BA00CF
8 changed files with 113 additions and 25 deletions

View File

@ -316,7 +316,6 @@ let me know by creating an issue!
- Validation
- Table joins
- Bulk inserts
- Custom column constraints
- When inserts / updates fail, explain why that happened
### Interesting stuff that would be nice to have
Implementing this will very likely result in backwards-incompatible changes.

View File

@ -1,8 +1,7 @@
import 'dart:typed_data';
import 'package:moor/moor.dart';
import 'package:moor/src/runtime/expressions/expression.dart';
import 'package:moor/src/runtime/expressions/comparable.dart';
import 'package:moor/src/types/sql_types.dart';
abstract class Column<T, S extends SqlType<T>> extends Expression<T, S> {}
@ -47,6 +46,33 @@ class ColumnBuilder<Builder, ResultColumn> {
/// primary key. Columns are non-null by default.
Builder nullable() => null;
/// Tells moor to write a custom constraint after this column definition when
/// writing this column, for instance in a CREATE TABLE statement.
///
/// When no custom constraint is set, columns will be written like this:
/// `name TYPE NULLABILITY NATIVE_CONSTRAINTS`. Native constraints are used to
/// enforce that booleans are either 0 or 1 (e.g.
/// `field BOOLEAN NOT NULL CHECK (field in (0, 1)`). Auto-Increment
/// columns also make use of the native constraints.
/// If [customConstraint] has been called, the nullability information and
/// native constraints will never be written. Instead, they will be replaced
/// with the [constraint]. For example, if you call
/// `customConstraint('UNIQUE')` on an [IntColumn] named "votes", the
/// generated column definition will be `votes INTEGER UNIQUE`. Notice how the
/// nullability information is lost - you'll have to include it in
/// [constraint] if that is desired.
///
/// This can be used to implement constraints that moor does not (yet)
/// support (e.g. unique keys, etc.). If you've found a common use-case for
/// this, it should be considered a limitation of moor itself. Please feel
/// free to open an issue at https://github.com/simolus3/moor/issues/new to
/// report that.
///
/// See also:
/// - https://www.sqlite.org/syntax/column-constraint.html
/// - [GeneratedColumn.writeCustomConstraints]
Builder customConstraint(String constraint) => null;
/// Turns this column builder into a column. This method won't actually be
/// called in your code. Instead, moor_generator will take a look at your
/// source code to figure out your table structure.

View File

@ -79,6 +79,19 @@ abstract class Query<Table, DataClass> {
/// Applies a [where] statement so that the row with the same primary key as
/// [d] will be matched.
void whereSamePrimaryKey(DataClass d) {
assert(
table.$primaryKey != null && table.$primaryKey.isNotEmpty,
'When using Query.whereSamePrimaryKey, which is also called from '
'DeleteStatement.delete and UpdateStatement.replace, the affected table'
'must have a primary key. You can either specify a primary implicitly '
'by making an integer() column autoIncrement(), or by explictly '
'overriding the primaryKey getter in your table class. You\'ll also '
'have to re-run the code generation step.\n'
'Alternatively, if you\'re using DeleteStatement.delete or '
'UpdateStatement.replace, consider using DeleteStatement.go or '
'UpdateStatement.write respectively. In that case, you need to use a '
'custom where statement.');
final primaryKeys = table.$primaryKey.map((c) => c.$name);
final updatedFields = table.entityToSql(d, includeNulls: true);

View File

@ -16,7 +16,12 @@ abstract class GeneratedColumn<T, S extends SqlType<T>> extends Column<T, S> {
/// Whether null values are allowed for this column.
final bool $nullable;
GeneratedColumn(this.$name, this.$nullable);
/// If custom constraints have been specified for this column via
/// [ColumnBuilder.customConstraint], these are kept here. Otherwise, this
/// field is going to be null.
final String $customConstraints;
GeneratedColumn(this.$name, this.$nullable, {this.$customConstraints});
/// Writes the definition of this column, as defined
/// [here](https://www.sqlite.org/syntax/column-def.html), into the given
@ -56,8 +61,8 @@ class GeneratedTextColumn extends GeneratedColumn<String, StringType>
final int maxTextLength;
GeneratedTextColumn(String name, bool nullable,
{this.minTextLength, this.maxTextLength})
: super(name, nullable);
{this.minTextLength, this.maxTextLength, String $customConstraints})
: super(name, nullable, $customConstraints: $customConstraints);
@override
Expression<bool, BoolType> like(String pattern) =>
@ -81,7 +86,8 @@ class GeneratedTextColumn extends GeneratedColumn<String, StringType>
class GeneratedBoolColumn extends GeneratedColumn<bool, BoolType>
implements BoolColumn {
GeneratedBoolColumn(String name, bool nullable) : super(name, nullable);
GeneratedBoolColumn(String name, bool nullable, {String $customConstraints})
: super(name, nullable, $customConstraints: $customConstraints);
@override
final String typeName = 'BOOLEAN';
@ -108,8 +114,8 @@ class GeneratedIntColumn extends GeneratedColumn<int, IntType>
final String typeName = 'INTEGER';
GeneratedIntColumn(String name, bool nullable,
{this.hasAutoIncrement = false})
: super(name, nullable);
{this.hasAutoIncrement = false, String $customConstraints})
: super(name, nullable, $customConstraints: $customConstraints);
@override
void writeColumnDefinition(StringBuffer into) {
@ -127,8 +133,9 @@ class GeneratedIntColumn extends GeneratedColumn<int, IntType>
class GeneratedDateTimeColumn extends GeneratedColumn<DateTime, DateTimeType>
implements DateTimeColumn {
GeneratedDateTimeColumn(String $name, bool $nullable)
: super($name, $nullable);
GeneratedDateTimeColumn(String $name, bool $nullable,
{String $customConstraints})
: super($name, $nullable, $customConstraints: $customConstraints);
@override
String get typeName => 'INTEGER'; // date-times are stored as unix-timestamps
@ -136,7 +143,8 @@ class GeneratedDateTimeColumn extends GeneratedColumn<DateTime, DateTimeType>
class GeneratedBlobColumn extends GeneratedColumn<Uint8List, BlobType>
implements BlobColumn {
GeneratedBlobColumn(String $name, bool $nullable) : super($name, $nullable);
GeneratedBlobColumn(String $name, bool $nullable, {String $customConstraints})
: super($name, $nullable, $customConstraints: $customConstraints);
@override
final String typeName = 'BLOB';

View File

@ -316,7 +316,6 @@ let me know by creating an issue!
- Validation
- Table joins
- Bulk inserts
- Custom column constraints
- When inserts / updates fail, explain why that happened
### Interesting stuff that would be nice to have
Implementing this will very likely result in backwards-incompatible changes.

View File

@ -63,6 +63,10 @@ class SpecifiedColumn {
final bool declaredAsPrimaryKey;
final List<ColumnFeature> features;
/// If this columns has custom constraints that should be used instead of the
/// default ones.
final String customConstraints;
/// The dart type that matches the values of this column. For instance, if a
/// table has declared an `IntColumn`, the matching dart type name would be [int].
String get dartTypeName => {
@ -105,13 +109,15 @@ class SpecifiedColumn {
ColumnType.blob: 'BlobType',
}[type];
const SpecifiedColumn(
{this.type,
this.dartGetterName,
this.name,
this.declaredAsPrimaryKey = false,
this.nullable = false,
this.features = const []});
const SpecifiedColumn({
this.type,
this.dartGetterName,
this.name,
this.customConstraints,
this.declaredAsPrimaryKey = false,
this.nullable = false,
this.features = const [],
});
}
abstract class ColumnFeature {

View File

@ -20,6 +20,7 @@ const String functionReferences = 'references';
const String functionAutoIncrement = 'autoIncrement';
const String functionWithLength = 'withLength';
const String functionNullable = 'nullable';
const String functionCustomConstraint = 'customConstraint';
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 '
@ -55,6 +56,7 @@ class ColumnParser extends ParserBase {
String foundStartMethod;
String foundExplicitName;
String foundCustomConstraint;
var wasDeclaredAsPrimaryKey = false;
var nullable = false;
// todo parse reference
@ -71,22 +73,28 @@ class ColumnParser extends ParserBase {
switch (methodName) {
case functionNamed:
if (foundExplicitName != null) {
generator.errors.add(MoorError(
generator.errors.add(
MoorError(
critical: false,
affectedElement: getter.declaredElement,
message:
"You're setting more than one name here, the first will "
'be used'));
'be used',
),
);
}
foundExplicitName =
readStringLiteral(remainingExpr.argumentList.arguments.first, () {
generator.errors.add(MoorError(
generator.errors.add(
MoorError(
critical: false,
affectedElement: getter.declaredElement,
message:
'This table name is cannot be resolved! Please only use '
'a constant string as parameter for .named().'));
'a constant string as parameter for .named().',
),
);
});
break;
case functionPrimaryKey:
@ -110,6 +118,21 @@ class ColumnParser extends ParserBase {
break;
case functionNullable:
nullable = true;
break;
case functionCustomConstraint:
foundCustomConstraint =
readStringLiteral(remainingExpr.argumentList.arguments.first, () {
generator.errors.add(
MoorError(
critical: false,
affectedElement: getter.declaredElement,
message:
'This constraint is cannot be resolved! Please only use '
'a constant string as parameter for .customConstraint().',
),
);
});
break;
}
// We're not at a starting method yet, so we need to go deeper!
@ -129,6 +152,7 @@ class ColumnParser extends ParserBase {
dartGetterName: getter.name.name,
name: name.escapeIfSqlKeyword(),
declaredAsPrimaryKey: wasDeclaredAsPrimaryKey,
customConstraints: foundCustomConstraint,
nullable: nullable,
features: foundFeatures);
}

View File

@ -29,7 +29,7 @@ void main() async {
class CustomPrimaryKey extends Table {
IntColumn get partA => integer()();
IntColumn get partB => integer()();
IntColumn get partB => integer().customConstraint('custom')();
@override
Set<Column> get primaryKey => {partA, partB};
@ -109,6 +109,19 @@ void main() async {
expect(
idColumn.features, contains(LimitingTextLength.withLength(max: 100)));
});
test('parses custom constraints', () {
final table =
TableParser(generator).parse(testLib.getType('CustomPrimaryKey'));
final partA =
table.columns.singleWhere((c) => c.dartGetterName == 'partA');
final partB =
table.columns.singleWhere((c) => c.dartGetterName == 'partB');
expect(partB.customConstraints, 'custom');
expect(partA.customConstraints, isNull);
});
});
test('parses custom primary keys', () {