From 28130fd3f174fa909ada09fa86b37e8af47ce5ef Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sat, 7 Oct 2023 22:30:45 +0200 Subject: [PATCH] Document custom types --- .../modular/custom_types/drift_table.drift | 7 + .../custom_types/drift_table.drift.dart | 223 ++++++++++++++++++ .../snippets/modular/custom_types/table.dart | 9 + .../modular/custom_types/table.drift.dart | 221 +++++++++++++++++ .../snippets/modular/custom_types/type.dart | 19 ++ .../modular/many_to_many/json.drift.dart | 1 + docs/pages/docs/SQL API/types.md | 86 +++++++ drift/lib/src/dsl/table.dart | 7 + drift/lib/src/runtime/types/mapping.dart | 2 + 9 files changed, 575 insertions(+) create mode 100644 docs/lib/snippets/modular/custom_types/drift_table.drift create mode 100644 docs/lib/snippets/modular/custom_types/drift_table.drift.dart create mode 100644 docs/lib/snippets/modular/custom_types/table.dart create mode 100644 docs/lib/snippets/modular/custom_types/table.drift.dart create mode 100644 docs/lib/snippets/modular/custom_types/type.dart create mode 100644 docs/pages/docs/SQL API/types.md diff --git a/docs/lib/snippets/modular/custom_types/drift_table.drift b/docs/lib/snippets/modular/custom_types/drift_table.drift new file mode 100644 index 00000000..92317018 --- /dev/null +++ b/docs/lib/snippets/modular/custom_types/drift_table.drift @@ -0,0 +1,7 @@ +import 'type.dart'; + +CREATE TABLE periodic_reminders ( + id INTEGER NOT NULL PRIMARY KEY, + frequency `const DurationType()` NOT NULL, + reminder TEXT NOT NULL +); diff --git a/docs/lib/snippets/modular/custom_types/drift_table.drift.dart b/docs/lib/snippets/modular/custom_types/drift_table.drift.dart new file mode 100644 index 00000000..db590906 --- /dev/null +++ b/docs/lib/snippets/modular/custom_types/drift_table.drift.dart @@ -0,0 +1,223 @@ +// ignore_for_file: type=lint +import 'package:drift/drift.dart' as i0; +import 'package:drift_docs/snippets/modular/custom_types/drift_table.drift.dart' + as i1; +import 'package:drift_docs/snippets/modular/custom_types/type.dart' as i2; + +class PeriodicReminders extends i0.Table + with i0.TableInfo { + @override + final i0.GeneratedDatabase attachedDatabase; + final String? _alias; + PeriodicReminders(this.attachedDatabase, [this._alias]); + static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id'); + late final i0.GeneratedColumn id = i0.GeneratedColumn( + 'id', aliasedName, false, + type: i0.DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL PRIMARY KEY'); + static const i0.VerificationMeta _frequencyMeta = + const i0.VerificationMeta('frequency'); + late final i0.GeneratedColumn frequency = + i0.GeneratedColumn('frequency', aliasedName, false, + type: const i2.DurationType(), + requiredDuringInsert: true, + $customConstraints: 'NOT NULL'); + static const i0.VerificationMeta _reminderMeta = + const i0.VerificationMeta('reminder'); + late final i0.GeneratedColumn reminder = i0.GeneratedColumn( + 'reminder', aliasedName, false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL'); + @override + List get $columns => [id, frequency, reminder]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'periodic_reminders'; + @override + i0.VerificationContext validateIntegrity( + i0.Insertable instance, + {bool isInserting = false}) { + final context = i0.VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } + if (data.containsKey('frequency')) { + context.handle(_frequencyMeta, + frequency.isAcceptableOrUnknown(data['frequency']!, _frequencyMeta)); + } else if (isInserting) { + context.missing(_frequencyMeta); + } + if (data.containsKey('reminder')) { + context.handle(_reminderMeta, + reminder.isAcceptableOrUnknown(data['reminder']!, _reminderMeta)); + } else if (isInserting) { + context.missing(_reminderMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + i1.PeriodicReminder map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return i1.PeriodicReminder( + id: attachedDatabase.typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}id'])!, + frequency: attachedDatabase.typeMapping + .read(const i2.DurationType(), data['${effectivePrefix}frequency'])!, + reminder: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}reminder'])!, + ); + } + + @override + PeriodicReminders createAlias(String alias) { + return PeriodicReminders(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class PeriodicReminder extends i0.DataClass + implements i0.Insertable { + final int id; + final Duration frequency; + final String reminder; + const PeriodicReminder( + {required this.id, required this.frequency, required this.reminder}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = i0.Variable(id); + map['frequency'] = i0.Variable(frequency); + map['reminder'] = i0.Variable(reminder); + return map; + } + + i1.PeriodicRemindersCompanion toCompanion(bool nullToAbsent) { + return i1.PeriodicRemindersCompanion( + id: i0.Value(id), + frequency: i0.Value(frequency), + reminder: i0.Value(reminder), + ); + } + + factory PeriodicReminder.fromJson(Map json, + {i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return PeriodicReminder( + id: serializer.fromJson(json['id']), + frequency: serializer.fromJson(json['frequency']), + reminder: serializer.fromJson(json['reminder']), + ); + } + @override + Map toJson({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'frequency': serializer.toJson(frequency), + 'reminder': serializer.toJson(reminder), + }; + } + + i1.PeriodicReminder copyWith( + {int? id, Duration? frequency, String? reminder}) => + i1.PeriodicReminder( + id: id ?? this.id, + frequency: frequency ?? this.frequency, + reminder: reminder ?? this.reminder, + ); + @override + String toString() { + return (StringBuffer('PeriodicReminder(') + ..write('id: $id, ') + ..write('frequency: $frequency, ') + ..write('reminder: $reminder') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, frequency, reminder); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is i1.PeriodicReminder && + other.id == this.id && + other.frequency == this.frequency && + other.reminder == this.reminder); +} + +class PeriodicRemindersCompanion + extends i0.UpdateCompanion { + final i0.Value id; + final i0.Value frequency; + final i0.Value reminder; + const PeriodicRemindersCompanion({ + this.id = const i0.Value.absent(), + this.frequency = const i0.Value.absent(), + this.reminder = const i0.Value.absent(), + }); + PeriodicRemindersCompanion.insert({ + this.id = const i0.Value.absent(), + required Duration frequency, + required String reminder, + }) : frequency = i0.Value(frequency), + reminder = i0.Value(reminder); + static i0.Insertable custom({ + i0.Expression? id, + i0.Expression? frequency, + i0.Expression? reminder, + }) { + return i0.RawValuesInsertable({ + if (id != null) 'id': id, + if (frequency != null) 'frequency': frequency, + if (reminder != null) 'reminder': reminder, + }); + } + + i1.PeriodicRemindersCompanion copyWith( + {i0.Value? id, + i0.Value? frequency, + i0.Value? reminder}) { + return i1.PeriodicRemindersCompanion( + id: id ?? this.id, + frequency: frequency ?? this.frequency, + reminder: reminder ?? this.reminder, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = i0.Variable(id.value); + } + if (frequency.present) { + map['frequency'] = + i0.Variable(frequency.value, const i2.DurationType()); + } + if (reminder.present) { + map['reminder'] = i0.Variable(reminder.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PeriodicRemindersCompanion(') + ..write('id: $id, ') + ..write('frequency: $frequency, ') + ..write('reminder: $reminder') + ..write(')')) + .toString(); + } +} diff --git a/docs/lib/snippets/modular/custom_types/table.dart b/docs/lib/snippets/modular/custom_types/table.dart new file mode 100644 index 00000000..80790571 --- /dev/null +++ b/docs/lib/snippets/modular/custom_types/table.dart @@ -0,0 +1,9 @@ +import 'package:drift/drift.dart'; +import 'type.dart'; + +class PeriodicReminders extends Table { + IntColumn get id => integer().autoIncrement()(); + Column get frequency => customType(const DurationType()) + .clientDefault(() => Duration(minutes: 15))(); + TextColumn get reminder => text()(); +} diff --git a/docs/lib/snippets/modular/custom_types/table.drift.dart b/docs/lib/snippets/modular/custom_types/table.drift.dart new file mode 100644 index 00000000..2e3239e1 --- /dev/null +++ b/docs/lib/snippets/modular/custom_types/table.drift.dart @@ -0,0 +1,221 @@ +// ignore_for_file: type=lint +import 'package:drift/drift.dart' as i0; +import 'package:drift_docs/snippets/modular/custom_types/table.drift.dart' + as i1; +import 'package:drift_docs/snippets/modular/custom_types/type.dart' as i2; +import 'package:drift_docs/snippets/modular/custom_types/table.dart' as i3; + +class $PeriodicRemindersTable extends i3.PeriodicReminders + with i0.TableInfo<$PeriodicRemindersTable, i1.PeriodicReminder> { + @override + final i0.GeneratedDatabase attachedDatabase; + final String? _alias; + $PeriodicRemindersTable(this.attachedDatabase, [this._alias]); + static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id'); + @override + late final i0.GeneratedColumn id = i0.GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: i0.DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + i0.GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + static const i0.VerificationMeta _frequencyMeta = + const i0.VerificationMeta('frequency'); + @override + late final i0.GeneratedColumn frequency = + i0.GeneratedColumn('frequency', aliasedName, false, + type: const i2.DurationType(), + requiredDuringInsert: false, + clientDefault: () => Duration(minutes: 15)); + static const i0.VerificationMeta _reminderMeta = + const i0.VerificationMeta('reminder'); + @override + late final i0.GeneratedColumn reminder = i0.GeneratedColumn( + 'reminder', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + @override + List get $columns => [id, frequency, reminder]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'periodic_reminders'; + @override + i0.VerificationContext validateIntegrity( + i0.Insertable instance, + {bool isInserting = false}) { + final context = i0.VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } + if (data.containsKey('frequency')) { + context.handle(_frequencyMeta, + frequency.isAcceptableOrUnknown(data['frequency']!, _frequencyMeta)); + } + if (data.containsKey('reminder')) { + context.handle(_reminderMeta, + reminder.isAcceptableOrUnknown(data['reminder']!, _reminderMeta)); + } else if (isInserting) { + context.missing(_reminderMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + i1.PeriodicReminder map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return i1.PeriodicReminder( + id: attachedDatabase.typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}id'])!, + frequency: attachedDatabase.typeMapping + .read(const i2.DurationType(), data['${effectivePrefix}frequency'])!, + reminder: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}reminder'])!, + ); + } + + @override + $PeriodicRemindersTable createAlias(String alias) { + return $PeriodicRemindersTable(attachedDatabase, alias); + } +} + +class PeriodicReminder extends i0.DataClass + implements i0.Insertable { + final int id; + final Duration frequency; + final String reminder; + const PeriodicReminder( + {required this.id, required this.frequency, required this.reminder}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = i0.Variable(id); + map['frequency'] = i0.Variable(frequency); + map['reminder'] = i0.Variable(reminder); + return map; + } + + i1.PeriodicRemindersCompanion toCompanion(bool nullToAbsent) { + return i1.PeriodicRemindersCompanion( + id: i0.Value(id), + frequency: i0.Value(frequency), + reminder: i0.Value(reminder), + ); + } + + factory PeriodicReminder.fromJson(Map json, + {i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return PeriodicReminder( + id: serializer.fromJson(json['id']), + frequency: serializer.fromJson(json['frequency']), + reminder: serializer.fromJson(json['reminder']), + ); + } + @override + Map toJson({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'frequency': serializer.toJson(frequency), + 'reminder': serializer.toJson(reminder), + }; + } + + i1.PeriodicReminder copyWith( + {int? id, Duration? frequency, String? reminder}) => + i1.PeriodicReminder( + id: id ?? this.id, + frequency: frequency ?? this.frequency, + reminder: reminder ?? this.reminder, + ); + @override + String toString() { + return (StringBuffer('PeriodicReminder(') + ..write('id: $id, ') + ..write('frequency: $frequency, ') + ..write('reminder: $reminder') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, frequency, reminder); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is i1.PeriodicReminder && + other.id == this.id && + other.frequency == this.frequency && + other.reminder == this.reminder); +} + +class PeriodicRemindersCompanion + extends i0.UpdateCompanion { + final i0.Value id; + final i0.Value frequency; + final i0.Value reminder; + const PeriodicRemindersCompanion({ + this.id = const i0.Value.absent(), + this.frequency = const i0.Value.absent(), + this.reminder = const i0.Value.absent(), + }); + PeriodicRemindersCompanion.insert({ + this.id = const i0.Value.absent(), + this.frequency = const i0.Value.absent(), + required String reminder, + }) : reminder = i0.Value(reminder); + static i0.Insertable custom({ + i0.Expression? id, + i0.Expression? frequency, + i0.Expression? reminder, + }) { + return i0.RawValuesInsertable({ + if (id != null) 'id': id, + if (frequency != null) 'frequency': frequency, + if (reminder != null) 'reminder': reminder, + }); + } + + i1.PeriodicRemindersCompanion copyWith( + {i0.Value? id, + i0.Value? frequency, + i0.Value? reminder}) { + return i1.PeriodicRemindersCompanion( + id: id ?? this.id, + frequency: frequency ?? this.frequency, + reminder: reminder ?? this.reminder, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = i0.Variable(id.value); + } + if (frequency.present) { + map['frequency'] = + i0.Variable(frequency.value, const i2.DurationType()); + } + if (reminder.present) { + map['reminder'] = i0.Variable(reminder.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PeriodicRemindersCompanion(') + ..write('id: $id, ') + ..write('frequency: $frequency, ') + ..write('reminder: $reminder') + ..write(')')) + .toString(); + } +} diff --git a/docs/lib/snippets/modular/custom_types/type.dart b/docs/lib/snippets/modular/custom_types/type.dart new file mode 100644 index 00000000..4acc263a --- /dev/null +++ b/docs/lib/snippets/modular/custom_types/type.dart @@ -0,0 +1,19 @@ +import 'package:drift/drift.dart'; + +class DurationType implements CustomSqlType { + const DurationType(); + + @override + String mapToSqlLiteral(Duration dartValue) { + return "interval '${dartValue.inMicroseconds} microseconds'"; + } + + @override + Object mapToSqlParameter(Duration dartValue) => dartValue; + + @override + Duration read(Object fromSql) => fromSql as Duration; + + @override + String sqlTypeName(GenerationContext context) => 'interval'; +} diff --git a/docs/lib/snippets/modular/many_to_many/json.drift.dart b/docs/lib/snippets/modular/many_to_many/json.drift.dart index e5542372..57ddbccf 100644 --- a/docs/lib/snippets/modular/many_to_many/json.drift.dart +++ b/docs/lib/snippets/modular/many_to_many/json.drift.dart @@ -187,6 +187,7 @@ class ShoppingCartsCompanion extends i0.UpdateCompanion { } if (entries.present) { final converter = i2.$ShoppingCartsTable.$converterentries; + map['entries'] = i0.Variable(converter.toSql(entries.value)); } return map; diff --git a/docs/pages/docs/SQL API/types.md b/docs/pages/docs/SQL API/types.md new file mode 100644 index 00000000..c868c797 --- /dev/null +++ b/docs/pages/docs/SQL API/types.md @@ -0,0 +1,86 @@ +--- +data: + title: "Custom SQL types" + weight: 10 + description: Use custom SQL types in Drift files and Dart code. + +template: layouts/docs/single +--- + +Drift's core library is written with sqlite3 as a primary target. This is +reflected in the [SQL types][types] drift supports out of the box - these +types supported by sqlite3 with a few additions that are handled in Dart. + +Other databases for which drift has limited support commonly support more types. +For instance, postgres has a dedicated type for durations, JSON values, UUIDs +and more. With a sqlite3 database, you'd use a [type converter][type converters] +to store these values with the types supported by sqlite3. +While type converters can also work here, they tell drift to use a regular text +column under the hood. When a database has builtin support for UUIDs for instance, +this could lead to less efficient statements or issues with other applications +talking to same database. +For this reason, drift allows the use of "custom types" - types that are not defined +in the core `drift` package and don't work with all databases. + +{% block "blocks/alert" title="When to use custom types - summary" %} +Custom types are a good tool when extending drift support to new database engines +with their own types not already covered by drift. + +Unless you're extending drift to work with a new database package (which is awesome, +please reach out!), you probably don't need to implement custom types yourself. +Packages like `drift_postgres` already define relevant custom types for you. +{% endblock %} + +## Defining a type + +As an example, let's assume we have a database with native support for `Duration` +values via the `interval` type. We're using a database driver that also has native +support for `Duration` values, meaning that they can be passed to the database in +prepared statements and also be read from rows without manual conversions. + +In that case, a custom type class to implement `Duration` support for drift would be +added: + +{% include "blocks/snippet" snippets = ('package:drift_docs/snippets/modular/custom_types/type.dart.excerpt.json' | readString | json_decode) %} + +This type defines the following things: + +- When `Duration` values are mapped to SQL literals (for instance, because they're used in `Constant`s), + we represent them as `interval '123754 microseconds'` in SQL. +- When a `Duration` value is mapped to a parameter, we just use the value directly (since we + assume it is supported by the underlying database driver here). +- Similarly, we expect that the database driver correctly returns durations as instances of + `Duration`, so the other way around in `read` also just casts the value. +- The name to use in `CREATE TABLE` statements and casts is `interval`. + +## Using custom types + +### In Dart + +To define a custom type on a Dart table, use the `customType` column builder method with the type: + +{% include "blocks/snippet" snippets = ('package:drift_docs/snippets/modular/custom_types/table.dart.excerpt.json' | readString | json_decode) %} + +As the example shows, other column constraints like `clientDefault` can still be added to custom +columns. You can even combine custom columns and type converters if needed. + +This is enough to get most queries to work, but in some advanced scenarios you may have to provide +more information to use custom types. +For instance, when manually constructing a `Variable` or a `Constant` with a custom type, the custom +type must be added as a second parameter to the constructor. This is because, unlike for builtin types, +drift doesn't have a central register describing how to deal with custom type values. + +### In SQL + +In SQL, Drift's [inline Dart]({{ 'drift_files.md#dart-interop' | pageUrl }}) syntax may be used to define +the custom type: + +{% include "blocks/snippet" snippets = ('package:drift_docs/snippets/modular/custom_types/drift_table.drift.excerpt.json' | readString | json_decode) %} + +Please note that support for custom types in drift files is currently limited. +For instance, custom types are not currently supported in `CAST` expressions. +If you are interested in advanced analysis support for custom types, please reach out by +opening an issue or a discussion describing your use-cases, thanks! + +[types]: {{ '../Dart API/tables.md#supported-column-types' | pageUrl }} +[type converters]: {{ '../type_converters.md' | pageUrl }} diff --git a/drift/lib/src/dsl/table.dart b/drift/lib/src/dsl/table.dart index c3938a78..1ca952c7 100644 --- a/drift/lib/src/dsl/table.dart +++ b/drift/lib/src/dsl/table.dart @@ -197,6 +197,13 @@ abstract class Table extends HasResultSet { @protected ColumnBuilder real() => _isGenerated(); + /// Defines a column with a custom [type] when used as a getter. + /// + /// For more information on custom types and when they can be useful, see + /// https://drift.simonbinder.eu/docs/sql-api/types/. + /// + /// For most users, [TypeConverter]s are a more appropriate tool to store + /// custom values in the database. @protected ColumnBuilder customType(CustomSqlType type) => _isGenerated(); diff --git a/drift/lib/src/runtime/types/mapping.dart b/drift/lib/src/runtime/types/mapping.dart index 44c6325d..f3c77a0b 100644 --- a/drift/lib/src/runtime/types/mapping.dart +++ b/drift/lib/src/runtime/types/mapping.dart @@ -407,6 +407,8 @@ enum DriftSqlType implements BaseSqlType { /// To create a custom type, implement this interface. You can now create values /// of this type by passing it to [Constant] or [Variable], [Expression.cast] /// and other methods operating on types. +/// Custom types can also be applied to table columns, see https://drift.simonbinder.eu/docs/sql-api/types/ +/// for details. abstract interface class CustomSqlType implements BaseSqlType { /// Interprets the underlying [fromSql] value from the database driver into