mirror of https://github.com/AMT-Cheif/drift.git
Merge pull request #1774 from westito/unique-constraint
Unique constraint DSL for Dart tables
This commit is contained in:
commit
5981d409c5
|
@ -0,0 +1,17 @@
|
|||
import 'package:drift/drift.dart';
|
||||
|
||||
// #docregion unique
|
||||
class WithUniqueConstraints extends Table {
|
||||
IntColumn get a => integer().unique()();
|
||||
|
||||
IntColumn get b => integer()();
|
||||
IntColumn get c => integer()();
|
||||
|
||||
@override
|
||||
List<Set<Column>> get uniqueKeys => [
|
||||
{b, c}
|
||||
];
|
||||
|
||||
// Effectively, this table has two unique key sets: (a) and (b, c).
|
||||
}
|
||||
// #enddocregion unique
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
data:
|
||||
title: "Dart tables"
|
||||
description: Further information on Dart tables
|
||||
description: Further information on defining tables in Dart.
|
||||
weight: 150
|
||||
template: layouts/docs/single
|
||||
---
|
||||
|
@ -11,6 +11,8 @@ __Prefer sql?__ If you prefer, you can also declare tables via `CREATE TABLE` st
|
|||
Drift's sql analyzer will generate matching Dart code. [Details]({{ "starting_with_sql.md" | pageUrl }}).
|
||||
{% endblock %}
|
||||
|
||||
{% assign snippets = 'package:moor_documentation/snippets/tables/advanced.dart.excerpt.json' | readString | json_decode %}
|
||||
|
||||
As shown in the [getting started guide]({{ "index.md" | pageUrl }}), sql tables can be written in Dart:
|
||||
```dart
|
||||
class Todos extends Table {
|
||||
|
@ -128,6 +130,19 @@ Note that the primary key must essentially be constant so that the generator can
|
|||
- it must be defined with the `=>` syntax, function bodies aren't supported
|
||||
- it must return a set literal without collection elements like `if`, `for` or spread operators
|
||||
|
||||
## Unique Constraints
|
||||
|
||||
Starting from version 1.6.0, `UNIQUE` SQL constraints can be defined on Dart tables too.
|
||||
A unique constraint contains one or more columns. The combination of all columns in a constraint
|
||||
must be unique in the table, or the databas will report an error on inserts.
|
||||
|
||||
With drift, a unique constraint can be added to a single column by marking it as `.unique()` in
|
||||
the column builder.
|
||||
A unique set spanning multiple columns can be added by overriding the `uniqueKeys` getter in the
|
||||
`Table` class:
|
||||
|
||||
{% include "blocks/snippet" snippets = snippets name = 'unique' %}
|
||||
|
||||
## Supported column types
|
||||
|
||||
Drift supports a variety of column types out of the box. You can store custom classes in columns by using
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
## 1.6.0-dev
|
||||
|
||||
- Add the `unique()` method to columns and the `uniqueKeys` override to tables
|
||||
to define unique constraints in Dart tables.
|
||||
- Add the very experimental `package:drift/wasm.dart` library. It uses WebAssembly
|
||||
to access sqlite3 without any external JavaScript libraries, but requires you to
|
||||
add a [WebAssembly module](https://github.com/simolus3/sqlite3.dart/tree/main/sqlite3#wasm-web-support)
|
||||
|
|
|
@ -172,6 +172,8 @@ class $TodoCategoriesTable extends TodoCategories
|
|||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {id};
|
||||
@override
|
||||
List<Set<GeneratedColumn>> get uniqueKeys => [];
|
||||
@override
|
||||
TodoCategory map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
return TodoCategory.fromData(data,
|
||||
prefix: tablePrefix != null ? '$tablePrefix.' : null);
|
||||
|
@ -460,6 +462,8 @@ class $TodoItemsTable extends TodoItems
|
|||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {id};
|
||||
@override
|
||||
List<Set<GeneratedColumn>> get uniqueKeys => [];
|
||||
@override
|
||||
TodoItem map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
return TodoItem.fromData(data,
|
||||
prefix: tablePrefix != null ? '$tablePrefix.' : null);
|
||||
|
|
|
@ -95,8 +95,8 @@ extension BuildColumn<T> on ColumnBuilder<T> {
|
|||
/// [constraint] if that is desired.
|
||||
///
|
||||
/// This can be used to implement constraints that drift 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 drift itself. Please feel
|
||||
/// support. If you've found a common use-case for this, it should be
|
||||
/// considered a limitation of drift itself. Please feel
|
||||
/// free to open an issue at https://github.com/simolus3/drift/issues/new to
|
||||
/// report that.
|
||||
///
|
||||
|
@ -252,6 +252,12 @@ extension BuildGeneralColumn<T> on _BaseColumnBuilder<T> {
|
|||
/// primary key. Columns are non-null by default.
|
||||
ColumnBuilder<T?> nullable() => _isGenerated();
|
||||
|
||||
/// Adds UNIQUE constraint to column.
|
||||
///
|
||||
/// Unique constraints spanning multiple keys can be added to a table by
|
||||
/// overriding [Table.uniqueKeys].
|
||||
ColumnBuilder<T?> unique() => _isGenerated();
|
||||
|
||||
/// Uses a custom [converter] to store custom Dart objects in a single column
|
||||
/// and automatically mapping them from and to sql.
|
||||
///
|
||||
|
|
|
@ -62,6 +62,34 @@ abstract class Table extends HasResultSet {
|
|||
@visibleForOverriding
|
||||
Set<Column>? get primaryKey => null;
|
||||
|
||||
/// Unique constraints in this table.
|
||||
///
|
||||
/// When two rows have the same value in _any_ set specified in [uniqueKeys],
|
||||
/// the database will reject the second row for inserts.
|
||||
///
|
||||
/// Override this to specify unique keys:
|
||||
///
|
||||
/// ```dart
|
||||
/// class IngredientInRecipes extends Table {
|
||||
/// @override
|
||||
/// Set<Column> get uniqueKeys =>
|
||||
/// [{recipe, ingredient}, {recipe, amountInGrams}];
|
||||
///
|
||||
/// IntColumn get recipe => integer()();
|
||||
/// IntColumn get ingredient => integer()();
|
||||
///
|
||||
/// IntColumn get amountInGrams => integer().named('amount')();
|
||||
/// ```
|
||||
///
|
||||
/// The getter must return a list of set literals using the `=>` syntax so
|
||||
/// that the drift generator can understand the code.
|
||||
///
|
||||
/// Note that individual columns can also be marked as unique with
|
||||
/// [BuildGeneralColumn.unique]. This is equivalent to adding a single-element
|
||||
/// set to this list.
|
||||
@visibleForOverriding
|
||||
List<Set<Column>>? get uniqueKeys => null;
|
||||
|
||||
/// Custom table constraints that should be added to the table.
|
||||
///
|
||||
/// See also:
|
||||
|
|
|
@ -288,6 +288,21 @@ class Migrator {
|
|||
context.buffer.write(')');
|
||||
}
|
||||
|
||||
if (table.uniqueKeys.isNotEmpty) {
|
||||
for (final key in table.uniqueKeys) {
|
||||
context.buffer.write(', UNIQUE (');
|
||||
final uqList = key.toList(growable: false);
|
||||
for (var i = 0; i < uqList.length; i++) {
|
||||
final column = uqList[i];
|
||||
|
||||
context.buffer.write(escapeIfNeeded(column.name));
|
||||
|
||||
if (i != uqList.length - 1) context.buffer.write(', ');
|
||||
}
|
||||
context.buffer.write(')');
|
||||
}
|
||||
}
|
||||
|
||||
final constraints = dslTable.customConstraints;
|
||||
|
||||
for (var i = 0; i < constraints.length; i++) {
|
||||
|
|
|
@ -28,6 +28,14 @@ mixin TableInfo<TableDsl extends Table, D> on Table
|
|||
@override
|
||||
Set<Column> get primaryKey => $primaryKey;
|
||||
|
||||
/// The unique key of this table. Can be empty if no custom primary key has
|
||||
/// been specified.
|
||||
///
|
||||
/// Additional to the [Table.primaryKey] columns declared by an user, this
|
||||
/// also contains auto-increment integers, which are primary key by default.
|
||||
@override
|
||||
List<Set<GeneratedColumn>> get uniqueKeys => const [];
|
||||
|
||||
/// The table name in the sql table. This can be an alias for the actual table
|
||||
/// name. See [actualTableName] for a table name that is not aliased.
|
||||
@Deprecated('Use aliasedName instead')
|
||||
|
|
|
@ -271,6 +271,8 @@ class ConfigTable extends Table with TableInfo<ConfigTable, Config> {
|
|||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {configKey};
|
||||
@override
|
||||
List<Set<GeneratedColumn>> get uniqueKeys => [];
|
||||
@override
|
||||
Config map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
return Config.fromData(data,
|
||||
prefix: tablePrefix != null ? '$tablePrefix.' : null);
|
||||
|
@ -458,6 +460,8 @@ class WithDefaults extends Table with TableInfo<WithDefaults, WithDefault> {
|
|||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => <GeneratedColumn>{};
|
||||
@override
|
||||
List<Set<GeneratedColumn>> get uniqueKeys => [];
|
||||
@override
|
||||
WithDefault map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
return WithDefault.fromData(data,
|
||||
prefix: tablePrefix != null ? '$tablePrefix.' : null);
|
||||
|
@ -546,6 +550,8 @@ class NoIds extends Table with TableInfo<NoIds, NoIdRow> {
|
|||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {payload};
|
||||
@override
|
||||
List<Set<GeneratedColumn>> get uniqueKeys => [];
|
||||
@override
|
||||
NoIdRow map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||
return NoIdRow(
|
||||
|
@ -767,6 +773,8 @@ class WithConstraints extends Table
|
|||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => <GeneratedColumn>{};
|
||||
@override
|
||||
List<Set<GeneratedColumn>> get uniqueKeys => [];
|
||||
@override
|
||||
WithConstraint map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
return WithConstraint.fromData(data,
|
||||
prefix: tablePrefix != null ? '$tablePrefix.' : null);
|
||||
|
@ -1036,6 +1044,8 @@ class Mytable extends Table with TableInfo<Mytable, MytableData> {
|
|||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {someid};
|
||||
@override
|
||||
List<Set<GeneratedColumn>> get uniqueKeys => [];
|
||||
@override
|
||||
MytableData map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
return MytableData.fromData(data,
|
||||
prefix: tablePrefix != null ? '$tablePrefix.' : null);
|
||||
|
@ -1256,6 +1266,8 @@ class Email extends Table
|
|||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => <GeneratedColumn>{};
|
||||
@override
|
||||
List<Set<GeneratedColumn>> get uniqueKeys => [];
|
||||
@override
|
||||
EMail map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
return EMail.fromData(data,
|
||||
prefix: tablePrefix != null ? '$tablePrefix.' : null);
|
||||
|
@ -1443,6 +1455,8 @@ class WeirdTable extends Table with TableInfo<WeirdTable, WeirdData> {
|
|||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => <GeneratedColumn>{};
|
||||
@override
|
||||
List<Set<GeneratedColumn>> get uniqueKeys => [];
|
||||
@override
|
||||
WeirdData map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
return WeirdData.fromData(data,
|
||||
prefix: tablePrefix != null ? '$tablePrefix.' : null);
|
||||
|
|
|
@ -16,13 +16,19 @@ class TodosTable extends Table with AutoIncrement {
|
|||
TextColumn get title => text().withLength(min: 4, max: 16).nullable()();
|
||||
TextColumn get content => text()();
|
||||
@JsonKey('target_date')
|
||||
DateTimeColumn get targetDate => dateTime().nullable()();
|
||||
DateTimeColumn get targetDate => dateTime().nullable().unique()();
|
||||
|
||||
IntColumn get category => integer().references(Categories, #id).nullable()();
|
||||
|
||||
@override
|
||||
List<Set<Column>>? get uniqueKeys => [
|
||||
{title, category},
|
||||
{title, targetDate},
|
||||
];
|
||||
}
|
||||
|
||||
class Users extends Table with AutoIncrement {
|
||||
TextColumn get name => text().withLength(min: 6, max: 32)();
|
||||
TextColumn get name => text().withLength(min: 6, max: 32).unique()();
|
||||
BoolColumn get isAwesome => boolean().withDefault(const Constant(true))();
|
||||
|
||||
BlobColumn get profilePicture => blob()();
|
||||
|
|
|
@ -249,6 +249,8 @@ class $CategoriesTable extends Categories
|
|||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {id};
|
||||
@override
|
||||
List<Set<GeneratedColumn>> get uniqueKeys => [];
|
||||
@override
|
||||
Category map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
return Category.fromData(data,
|
||||
prefix: tablePrefix != null ? '$tablePrefix.' : null);
|
||||
|
@ -503,7 +505,9 @@ class $TodosTableTable extends TodosTable
|
|||
@override
|
||||
late final GeneratedColumn<DateTime?> targetDate = GeneratedColumn<DateTime?>(
|
||||
'target_date', aliasedName, true,
|
||||
type: const IntType(), requiredDuringInsert: false);
|
||||
type: const IntType(),
|
||||
requiredDuringInsert: false,
|
||||
defaultConstraints: 'UNIQUE');
|
||||
final VerificationMeta _categoryMeta = const VerificationMeta('category');
|
||||
@override
|
||||
late final GeneratedColumn<int?> category = GeneratedColumn<int?>(
|
||||
|
@ -552,6 +556,11 @@ class $TodosTableTable extends TodosTable
|
|||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {id};
|
||||
@override
|
||||
List<Set<GeneratedColumn>> get uniqueKeys => [
|
||||
{title, category},
|
||||
{title, targetDate},
|
||||
];
|
||||
@override
|
||||
TodoEntry map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
return TodoEntry.fromData(data,
|
||||
prefix: tablePrefix != null ? '$tablePrefix.' : null);
|
||||
|
@ -782,7 +791,8 @@ class $UsersTable extends Users with TableInfo<$UsersTable, User> {
|
|||
additionalChecks:
|
||||
GeneratedColumn.checkTextLength(minTextLength: 6, maxTextLength: 32),
|
||||
type: const StringType(),
|
||||
requiredDuringInsert: true);
|
||||
requiredDuringInsert: true,
|
||||
defaultConstraints: 'UNIQUE');
|
||||
final VerificationMeta _isAwesomeMeta = const VerificationMeta('isAwesome');
|
||||
@override
|
||||
late final GeneratedColumn<bool?> isAwesome = GeneratedColumn<bool?>(
|
||||
|
@ -850,6 +860,8 @@ class $UsersTable extends Users with TableInfo<$UsersTable, User> {
|
|||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {id};
|
||||
@override
|
||||
List<Set<GeneratedColumn>> get uniqueKeys => [];
|
||||
@override
|
||||
User map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
return User.fromData(data,
|
||||
prefix: tablePrefix != null ? '$tablePrefix.' : null);
|
||||
|
@ -1030,6 +1042,8 @@ class $SharedTodosTable extends SharedTodos
|
|||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {todo, user};
|
||||
@override
|
||||
List<Set<GeneratedColumn>> get uniqueKeys => [];
|
||||
@override
|
||||
SharedTodo map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
return SharedTodo.fromData(data,
|
||||
prefix: tablePrefix != null ? '$tablePrefix.' : null);
|
||||
|
@ -1184,6 +1198,8 @@ class $TableWithoutPKTable extends TableWithoutPK
|
|||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => <GeneratedColumn>{};
|
||||
@override
|
||||
List<Set<GeneratedColumn>> get uniqueKeys => [];
|
||||
@override
|
||||
CustomRowClass map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||
return CustomRowClass.map(
|
||||
|
@ -1341,6 +1357,8 @@ class $PureDefaultsTable extends PureDefaults
|
|||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {txt};
|
||||
@override
|
||||
List<Set<GeneratedColumn>> get uniqueKeys => [];
|
||||
@override
|
||||
PureDefault map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
return PureDefault.fromData(data,
|
||||
prefix: tablePrefix != null ? '$tablePrefix.' : null);
|
||||
|
|
|
@ -61,7 +61,7 @@ void main() {
|
|||
);
|
||||
await db.into(db.users).insert(
|
||||
UsersCompanion.insert(
|
||||
name: 'User name',
|
||||
name: 'User name 2',
|
||||
profilePicture: Uint8List(0),
|
||||
creationTime: Value(secondTime)),
|
||||
);
|
||||
|
|
|
@ -19,7 +19,7 @@ void main() {
|
|||
b.insertAll(db.users, [
|
||||
for (var i = 0; i < 1000; i++)
|
||||
UsersCompanion.insert(
|
||||
name: 'user name', profilePicture: Uint8List(0)),
|
||||
name: 'user name $i', profilePicture: Uint8List(0)),
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
|
@ -22,8 +22,9 @@ void main() {
|
|||
verify(mockExecutor.runCustom(
|
||||
'CREATE TABLE IF NOT EXISTS todos '
|
||||
'(id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, title TEXT NULL, '
|
||||
'content TEXT NOT NULL, target_date INTEGER NULL, '
|
||||
'category INTEGER NULL REFERENCES categories (id));',
|
||||
'content TEXT NOT NULL, target_date INTEGER NULL UNIQUE, '
|
||||
'category INTEGER NULL REFERENCES categories (id), '
|
||||
'UNIQUE (title, category), UNIQUE (title, target_date));',
|
||||
[]));
|
||||
|
||||
verify(mockExecutor.runCustom(
|
||||
|
@ -39,7 +40,7 @@ void main() {
|
|||
verify(mockExecutor.runCustom(
|
||||
'CREATE TABLE IF NOT EXISTS users '
|
||||
'(id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, '
|
||||
'name TEXT NOT NULL, '
|
||||
'name TEXT NOT NULL UNIQUE, '
|
||||
'is_awesome INTEGER NOT NULL DEFAULT 1 CHECK (is_awesome IN (0, 1)), '
|
||||
'profile_picture BLOB NOT NULL, '
|
||||
'creation_time INTEGER NOT NULL '
|
||||
|
@ -91,7 +92,7 @@ void main() {
|
|||
verify(mockExecutor.runCustom(
|
||||
'CREATE TABLE IF NOT EXISTS users '
|
||||
'(id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, '
|
||||
'name TEXT NOT NULL, '
|
||||
'name TEXT NOT NULL UNIQUE, '
|
||||
'is_awesome INTEGER NOT NULL DEFAULT 1 CHECK (is_awesome IN (0, 1)), '
|
||||
'profile_picture BLOB NOT NULL, '
|
||||
'creation_time INTEGER NOT NULL '
|
||||
|
|
|
@ -23,6 +23,7 @@ const String _methodReferences = 'references';
|
|||
const String _methodAutoIncrement = 'autoIncrement';
|
||||
const String _methodWithLength = 'withLength';
|
||||
const String _methodNullable = 'nullable';
|
||||
const String _methodUnique = 'unique';
|
||||
const String _methodCustomConstraint = 'customConstraint';
|
||||
const String _methodDefault = 'withDefault';
|
||||
const String _methodClientDefault = 'clientDefault';
|
||||
|
@ -218,6 +219,9 @@ class ColumnParser {
|
|||
case _methodNullable:
|
||||
nullable = true;
|
||||
break;
|
||||
case _methodUnique:
|
||||
foundFeatures.add(const UniqueKey());
|
||||
break;
|
||||
case _methodCustomConstraint:
|
||||
foundCustomConstraint = base.readStringLiteral(
|
||||
remainingExpr.argumentList.arguments.first, () {
|
||||
|
@ -355,6 +359,17 @@ class ColumnParser {
|
|||
);
|
||||
}
|
||||
|
||||
if (foundFeatures.contains(const UniqueKey()) &&
|
||||
foundFeatures.contains(const PrimaryKey())) {
|
||||
base.step.reportError(
|
||||
ErrorInDartCode(
|
||||
severity: Severity.error,
|
||||
affectedElement: getter.declaredElement,
|
||||
message: 'Primary key column cannot have UNIQUE constraint',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final docString =
|
||||
getter.documentationComment?.tokens.map((t) => t.toString()).join('\n');
|
||||
return MoorColumn(
|
||||
|
|
|
@ -12,6 +12,7 @@ class TableParser {
|
|||
|
||||
final columns = (await _parseColumns(element)).toList();
|
||||
final primaryKey = await _readPrimaryKey(element, columns);
|
||||
final uniqueKeys = await _readUniqueKeys(element, columns);
|
||||
|
||||
final dataClassInfo = _readDataClassInformation(columns, element);
|
||||
|
||||
|
@ -23,6 +24,7 @@ class TableParser {
|
|||
existingRowClass: dataClassInfo.existingClass,
|
||||
customParentClass: dataClassInfo.extending,
|
||||
primaryKey: primaryKey,
|
||||
uniqueKeys: uniqueKeys,
|
||||
overrideWithoutRowId: await _overrideWithoutRowId(element),
|
||||
declaration: DartTableDeclaration(element, base.step.file),
|
||||
);
|
||||
|
@ -34,6 +36,38 @@ class TableParser {
|
|||
));
|
||||
}
|
||||
|
||||
if (primaryKey != null &&
|
||||
primaryKey.length == 1 &&
|
||||
primaryKey.first.features.contains(const UniqueKey())) {
|
||||
base.step.errors.report(ErrorInDartCode(
|
||||
message: 'Primary key column cannot have UNIQUE constraint',
|
||||
affectedElement: element,
|
||||
));
|
||||
}
|
||||
|
||||
if (uniqueKeys != null &&
|
||||
uniqueKeys.any((key) =>
|
||||
uniqueKeys.length == 1 &&
|
||||
key.first.features.contains(const UniqueKey()))) {
|
||||
base.step.errors.report(ErrorInDartCode(
|
||||
message:
|
||||
'Column provided in a single-column uniqueKey set already has a '
|
||||
'column-level UNIQUE constraint',
|
||||
affectedElement: element,
|
||||
));
|
||||
}
|
||||
|
||||
if (uniqueKeys != null &&
|
||||
primaryKey != null &&
|
||||
uniqueKeys
|
||||
.any((unique) => const SetEquality().equals(unique, primaryKey))) {
|
||||
base.step.errors.report(ErrorInDartCode(
|
||||
message: 'The uniqueKeys override contains the primary key, which is '
|
||||
'already unique by default.',
|
||||
affectedElement: element,
|
||||
));
|
||||
}
|
||||
|
||||
var index = 0;
|
||||
for (final converter in table.converters) {
|
||||
converter
|
||||
|
@ -188,6 +222,68 @@ class TableParser {
|
|||
return parsedPrimaryKey;
|
||||
}
|
||||
|
||||
Future<List<Set<MoorColumn>>?> _readUniqueKeys(
|
||||
ClassElement element, List<MoorColumn> columns) async {
|
||||
final uniqueKeyGetter = element.lookUpGetter('uniqueKeys', element.library);
|
||||
|
||||
if (uniqueKeyGetter == null || uniqueKeyGetter.isFromDefaultTable) {
|
||||
// resolved uniqueKeys is from the Table dsl superclass. That means there
|
||||
// is no unique key list
|
||||
return null;
|
||||
}
|
||||
|
||||
final ast =
|
||||
await base.loadElementDeclaration(uniqueKeyGetter) as MethodDeclaration;
|
||||
final body = ast.body;
|
||||
if (body is! ExpressionFunctionBody) {
|
||||
base.step.reportError(ErrorInDartCode(
|
||||
affectedElement: uniqueKeyGetter,
|
||||
message: 'This must return a list of set literal using the => '
|
||||
'syntax!'));
|
||||
return null;
|
||||
}
|
||||
final expression = body.expression;
|
||||
final parsedUniqueKeys = <Set<MoorColumn>>[];
|
||||
|
||||
if (expression is ListLiteral) {
|
||||
for (final keySet in expression.elements) {
|
||||
if (keySet is SetOrMapLiteral) {
|
||||
final uniqueKey = <MoorColumn>{};
|
||||
for (final entry in keySet.elements) {
|
||||
if (entry is Identifier) {
|
||||
final column = columns.singleWhereOrNull(
|
||||
(column) => column.dartGetterName == entry.name);
|
||||
if (column == null) {
|
||||
base.step.reportError(
|
||||
ErrorInDartCode(
|
||||
affectedElement: uniqueKeyGetter,
|
||||
affectedNode: entry,
|
||||
message: 'Column not found in this table',
|
||||
),
|
||||
);
|
||||
} else {
|
||||
uniqueKey.add(column);
|
||||
}
|
||||
} else {
|
||||
print('Unexpected entry in expression.elements: $entry');
|
||||
}
|
||||
}
|
||||
parsedUniqueKeys.add(uniqueKey);
|
||||
} else {
|
||||
base.step.reportError(ErrorInDartCode(
|
||||
affectedElement: uniqueKeyGetter,
|
||||
message: 'This must return a set list literal!'));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
base.step.reportError(ErrorInDartCode(
|
||||
affectedElement: uniqueKeyGetter,
|
||||
message: 'This must return a set list literal!'));
|
||||
}
|
||||
|
||||
return parsedUniqueKeys;
|
||||
}
|
||||
|
||||
Future<bool?> _overrideWithoutRowId(ClassElement element) async {
|
||||
final getter = element.lookUpGetter('withoutRowId', element.library);
|
||||
|
||||
|
|
|
@ -203,6 +203,11 @@ class PrimaryKey extends ColumnFeature {
|
|||
const PrimaryKey();
|
||||
}
|
||||
|
||||
/// A `UNIQUE` column constraint.
|
||||
class UniqueKey extends ColumnFeature {
|
||||
const UniqueKey();
|
||||
}
|
||||
|
||||
class AutoIncrement extends PrimaryKey {
|
||||
static const AutoIncrement _instance = AutoIncrement._();
|
||||
|
||||
|
|
|
@ -92,6 +92,10 @@ class MoorTable extends MoorEntityWithResultSet {
|
|||
/// For the full primary key, see [fullPrimaryKey].
|
||||
final Set<MoorColumn>? primaryKey;
|
||||
|
||||
/// The set of unique keys if they have been explicitly defined by
|
||||
/// overriding `uniqueKeys` in the table class.
|
||||
final List<Set<MoorColumn>>? uniqueKeys;
|
||||
|
||||
/// The primary key for this table.
|
||||
///
|
||||
/// Unlikely [primaryKey], this method is not limited to the `primaryKey`
|
||||
|
@ -146,6 +150,7 @@ class MoorTable extends MoorEntityWithResultSet {
|
|||
required this.sqlName,
|
||||
required this.dartTypeName,
|
||||
this.primaryKey,
|
||||
this.uniqueKeys,
|
||||
String? overriddenName,
|
||||
this.overrideWithoutRowId,
|
||||
this.overrideTableConstraints,
|
||||
|
|
|
@ -28,9 +28,21 @@ abstract class TableOrViewWriter {
|
|||
: 'PRIMARY KEY');
|
||||
|
||||
wrotePkConstraint = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!wrotePkConstraint) {
|
||||
for (final feature in column.features) {
|
||||
if (feature is UniqueKey) {
|
||||
defaultConstraints.add('UNIQUE');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (final feature in column.features) {
|
||||
if (feature is ResolvedDartForeignKeyReference) {
|
||||
final tableName = escapeIfNeeded(feature.otherTable.sqlName);
|
||||
final columnName = escapeIfNeeded(feature.otherColumn.name.name);
|
||||
|
@ -324,6 +336,7 @@ class TableWriter extends TableOrViewWriter {
|
|||
|
||||
_writeValidityCheckMethod();
|
||||
_writePrimaryKeyOverride();
|
||||
_writeUniqueKeyOverride();
|
||||
|
||||
writeMappingMethod(scope);
|
||||
// _writeReverseMappingMethod();
|
||||
|
@ -425,6 +438,32 @@ class TableWriter extends TableOrViewWriter {
|
|||
buffer.write('};\n');
|
||||
}
|
||||
|
||||
void _writeUniqueKeyOverride() {
|
||||
buffer.write('@override\nList<Set<GeneratedColumn>> get uniqueKeys => ');
|
||||
final uniqueKeys = table.uniqueKeys ?? [];
|
||||
|
||||
if (uniqueKeys.isEmpty) {
|
||||
buffer.write('[];');
|
||||
return;
|
||||
}
|
||||
|
||||
buffer.write('[');
|
||||
for (final uniqueKey in uniqueKeys) {
|
||||
buffer.write('{');
|
||||
final uqList = uniqueKey.toList();
|
||||
for (var i = 0; i < uqList.length; i++) {
|
||||
final pk = uqList[i];
|
||||
|
||||
buffer.write(pk.dartGetterName);
|
||||
if (i != uqList.length - 1) {
|
||||
buffer.write(', ');
|
||||
}
|
||||
}
|
||||
buffer.write('},\n');
|
||||
}
|
||||
buffer.write('];\n');
|
||||
}
|
||||
|
||||
void _writeAliasGenerator() {
|
||||
final typeName = table.entityInfoName;
|
||||
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
import 'package:test/test.dart';
|
||||
|
||||
import '../utils.dart';
|
||||
|
||||
void main() {
|
||||
test('does not allow autoIncrement() to have a unique constraint', () async {
|
||||
final state = TestState.withContent({
|
||||
'a|lib/main.dart': '''
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
class Test extends Table {
|
||||
IntColumn get a => integer().autoIncrement().unique()();
|
||||
}
|
||||
'''
|
||||
});
|
||||
addTearDown(state.close);
|
||||
|
||||
(await state.analyze('package:a/main.dart')).expectDartError(
|
||||
'Primary key column cannot have UNIQUE constraint', 'a');
|
||||
});
|
||||
|
||||
test('does not allow primary key to have a unique constraint', () async {
|
||||
final state = TestState.withContent({
|
||||
'a|lib/main.dart': '''
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
class Test extends Table {
|
||||
IntColumn get a => integer().unique()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {a};
|
||||
}
|
||||
'''
|
||||
});
|
||||
addTearDown(state.close);
|
||||
|
||||
(await state.analyze('package:a/main.dart')).expectDartError(
|
||||
'Primary key column cannot have UNIQUE constraint', 'Test');
|
||||
});
|
||||
|
||||
test(
|
||||
'does not allow primary key to have a unique constraint through override',
|
||||
() async {
|
||||
final state = TestState.withContent({
|
||||
'a|lib/main.dart': '''
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
class Test extends Table {
|
||||
IntColumn get a => integer()();
|
||||
|
||||
@override
|
||||
List<Set<Column>> get uniqueKeys => [{a}];
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {a};
|
||||
}
|
||||
'''
|
||||
});
|
||||
addTearDown(state.close);
|
||||
|
||||
(await state.analyze('package:a/main.dart')).expectDartError(
|
||||
'The uniqueKeys override contains the primary key, which is already '
|
||||
'unique by default.',
|
||||
'Test');
|
||||
});
|
||||
|
||||
test('warns about duplicate unique declarations', () async {
|
||||
final state = TestState.withContent({
|
||||
'a|lib/main.dart': '''
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
class Test extends Table {
|
||||
IntColumn get a => integer().unique()();
|
||||
|
||||
@override
|
||||
List<Set<Column>> get uniqueKeys => [{a}];
|
||||
}
|
||||
'''
|
||||
});
|
||||
addTearDown(state.close);
|
||||
|
||||
(await state.analyze('package:a/main.dart')).expectDartError(
|
||||
contains('already has a column-level UNIQUE constraint'), 'Test');
|
||||
});
|
||||
|
||||
test('parses unique key definitions', () async {
|
||||
final state = TestState.withContent({
|
||||
'a|lib/main.dart': '''
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
class Test extends Table {
|
||||
IntColumn get a => integer().unique()();
|
||||
IntColumn get b => integer().unique()();
|
||||
|
||||
@override
|
||||
List<Set<Column>> get uniqueKeys => [{a}, {b}, {a, b}];
|
||||
}
|
||||
'''
|
||||
});
|
||||
addTearDown(state.close);
|
||||
|
||||
final file = await state.analyze('package:a/main.dart');
|
||||
expect(file.errors.errors, isEmpty);
|
||||
|
||||
final table = file.currentResult!.declaredTables.single;
|
||||
expect(table.uniqueKeys, hasLength(3));
|
||||
expect(table.uniqueKeys![0].map((e) => e.name.name), ['a']);
|
||||
expect(table.uniqueKeys![1].map((e) => e.name.name), ['b']);
|
||||
expect(table.uniqueKeys![2].map((e) => e.name.name), ['a', 'b']);
|
||||
});
|
||||
}
|
|
@ -166,6 +166,8 @@ class Entries extends Table with TableInfo<Entries, Entrie> {
|
|||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {id};
|
||||
@override
|
||||
List<Set<GeneratedColumn>> get uniqueKeys => [];
|
||||
@override
|
||||
Entrie map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
return Entrie.fromData(data,
|
||||
prefix: tablePrefix != null ? '$tablePrefix.' : null);
|
||||
|
|
|
@ -166,6 +166,8 @@ class $UsersTable extends Users with TableInfo<$UsersTable, User> {
|
|||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {id};
|
||||
@override
|
||||
List<Set<GeneratedColumn>> get uniqueKeys => [];
|
||||
@override
|
||||
User map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
return User.fromData(data,
|
||||
prefix: tablePrefix != null ? '$tablePrefix.' : null);
|
||||
|
@ -414,6 +416,8 @@ class Groups extends Table with TableInfo<Groups, Group> {
|
|||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {id};
|
||||
@override
|
||||
List<Set<GeneratedColumn>> get uniqueKeys => [];
|
||||
@override
|
||||
Group map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
return Group.fromData(data,
|
||||
prefix: tablePrefix != null ? '$tablePrefix.' : null);
|
||||
|
|
|
@ -166,6 +166,8 @@ class Entries extends Table with TableInfo<Entries, Entrie> {
|
|||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {id};
|
||||
@override
|
||||
List<Set<GeneratedColumn>> get uniqueKeys => [];
|
||||
@override
|
||||
Entrie map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
return Entrie.fromData(data,
|
||||
prefix: tablePrefix != null ? '$tablePrefix.' : null);
|
||||
|
|
|
@ -171,6 +171,8 @@ class Users extends Table with TableInfo<Users, User> {
|
|||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {id};
|
||||
@override
|
||||
List<Set<GeneratedColumn>> get uniqueKeys => [];
|
||||
@override
|
||||
User map(Map<String, dynamic> data, {String tablePrefix}) {
|
||||
return User.fromData(data,
|
||||
prefix: tablePrefix != null ? '$tablePrefix.' : null);
|
||||
|
|
|
@ -126,9 +126,10 @@ class KeyValuesCompanion extends UpdateCompanion<KeyValue> {
|
|||
|
||||
class $KeyValuesTable extends KeyValues
|
||||
with TableInfo<$KeyValuesTable, KeyValue> {
|
||||
final GeneratedDatabase _db;
|
||||
@override
|
||||
final GeneratedDatabase attachedDatabase;
|
||||
final String? _alias;
|
||||
$KeyValuesTable(this._db, [this._alias]);
|
||||
$KeyValuesTable(this.attachedDatabase, [this._alias]);
|
||||
final VerificationMeta _keyMeta = const VerificationMeta('key');
|
||||
@override
|
||||
late final GeneratedColumn<String?> key = GeneratedColumn<String?>(
|
||||
|
@ -168,6 +169,8 @@ class $KeyValuesTable extends KeyValues
|
|||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {key};
|
||||
@override
|
||||
List<Set<GeneratedColumn>> get uniqueKeys => [];
|
||||
@override
|
||||
KeyValue map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
return KeyValue.fromData(data,
|
||||
prefix: tablePrefix != null ? '$tablePrefix.' : null);
|
||||
|
@ -175,7 +178,7 @@ class $KeyValuesTable extends KeyValues
|
|||
|
||||
@override
|
||||
$KeyValuesTable createAlias(String alias) {
|
||||
return $KeyValuesTable(_db, alias);
|
||||
return $KeyValuesTable(attachedDatabase, alias);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -307,6 +307,8 @@ class $UsersTable extends Users with TableInfo<$UsersTable, User> {
|
|||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {id};
|
||||
@override
|
||||
List<Set<GeneratedColumn>> get uniqueKeys => [];
|
||||
@override
|
||||
User map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
return User.fromData(data,
|
||||
prefix: tablePrefix != null ? '$tablePrefix.' : null);
|
||||
|
@ -531,6 +533,8 @@ class $FriendshipsTable extends Friendships
|
|||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {firstUser, secondUser};
|
||||
@override
|
||||
List<Set<GeneratedColumn>> get uniqueKeys => [];
|
||||
@override
|
||||
Friendship map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
return Friendship.fromData(data,
|
||||
prefix: tablePrefix != null ? '$tablePrefix.' : null);
|
||||
|
|
|
@ -137,6 +137,8 @@ class $FoosTable extends Foos with TableInfo<$FoosTable, Foo> {
|
|||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {id};
|
||||
@override
|
||||
List<Set<GeneratedColumn>> get uniqueKeys => [];
|
||||
@override
|
||||
Foo map(Map<String, dynamic> data, {String tablePrefix}) {
|
||||
return Foo.fromData(data,
|
||||
prefix: tablePrefix != null ? '$tablePrefix.' : null);
|
||||
|
@ -278,6 +280,8 @@ class $BarsTable extends Bars with TableInfo<$BarsTable, Bar> {
|
|||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {id};
|
||||
@override
|
||||
List<Set<GeneratedColumn>> get uniqueKeys => [];
|
||||
@override
|
||||
Bar map(Map<String, dynamic> data, {String tablePrefix}) {
|
||||
return Bar.fromData(data,
|
||||
prefix: tablePrefix != null ? '$tablePrefix.' : null);
|
||||
|
|
Loading…
Reference in New Issue