Document custom types

This commit is contained in:
Simon Binder 2023-10-07 22:30:45 +02:00
parent 7f0488056c
commit 28130fd3f1
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
9 changed files with 575 additions and 0 deletions

View File

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

View File

@ -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<PeriodicReminders, i1.PeriodicReminder> {
@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<int> id = i0.GeneratedColumn<int>(
'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<Duration> frequency =
i0.GeneratedColumn<Duration>('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<String> reminder = i0.GeneratedColumn<String>(
'reminder', aliasedName, false,
type: i0.DriftSqlType.string,
requiredDuringInsert: true,
$customConstraints: 'NOT NULL');
@override
List<i0.GeneratedColumn> 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<i1.PeriodicReminder> 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<i0.GeneratedColumn> get $primaryKey => {id};
@override
i1.PeriodicReminder map(Map<String, dynamic> 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<i1.PeriodicReminder> {
final int id;
final Duration frequency;
final String reminder;
const PeriodicReminder(
{required this.id, required this.frequency, required this.reminder});
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
map['id'] = i0.Variable<int>(id);
map['frequency'] = i0.Variable<Duration>(frequency);
map['reminder'] = i0.Variable<String>(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<String, dynamic> json,
{i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return PeriodicReminder(
id: serializer.fromJson<int>(json['id']),
frequency: serializer.fromJson<Duration>(json['frequency']),
reminder: serializer.fromJson<String>(json['reminder']),
);
}
@override
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<int>(id),
'frequency': serializer.toJson<Duration>(frequency),
'reminder': serializer.toJson<String>(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<i1.PeriodicReminder> {
final i0.Value<int> id;
final i0.Value<Duration> frequency;
final i0.Value<String> 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<i1.PeriodicReminder> custom({
i0.Expression<int>? id,
i0.Expression<Duration>? frequency,
i0.Expression<String>? reminder,
}) {
return i0.RawValuesInsertable({
if (id != null) 'id': id,
if (frequency != null) 'frequency': frequency,
if (reminder != null) 'reminder': reminder,
});
}
i1.PeriodicRemindersCompanion copyWith(
{i0.Value<int>? id,
i0.Value<Duration>? frequency,
i0.Value<String>? reminder}) {
return i1.PeriodicRemindersCompanion(
id: id ?? this.id,
frequency: frequency ?? this.frequency,
reminder: reminder ?? this.reminder,
);
}
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
if (id.present) {
map['id'] = i0.Variable<int>(id.value);
}
if (frequency.present) {
map['frequency'] =
i0.Variable<Duration>(frequency.value, const i2.DurationType());
}
if (reminder.present) {
map['reminder'] = i0.Variable<String>(reminder.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('PeriodicRemindersCompanion(')
..write('id: $id, ')
..write('frequency: $frequency, ')
..write('reminder: $reminder')
..write(')'))
.toString();
}
}

View File

@ -0,0 +1,9 @@
import 'package:drift/drift.dart';
import 'type.dart';
class PeriodicReminders extends Table {
IntColumn get id => integer().autoIncrement()();
Column<Duration> get frequency => customType(const DurationType())
.clientDefault(() => Duration(minutes: 15))();
TextColumn get reminder => text()();
}

View File

@ -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<int> id = i0.GeneratedColumn<int>(
'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<Duration> frequency =
i0.GeneratedColumn<Duration>('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<String> reminder = i0.GeneratedColumn<String>(
'reminder', aliasedName, false,
type: i0.DriftSqlType.string, requiredDuringInsert: true);
@override
List<i0.GeneratedColumn> 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<i1.PeriodicReminder> 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<i0.GeneratedColumn> get $primaryKey => {id};
@override
i1.PeriodicReminder map(Map<String, dynamic> 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<i1.PeriodicReminder> {
final int id;
final Duration frequency;
final String reminder;
const PeriodicReminder(
{required this.id, required this.frequency, required this.reminder});
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
map['id'] = i0.Variable<int>(id);
map['frequency'] = i0.Variable<Duration>(frequency);
map['reminder'] = i0.Variable<String>(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<String, dynamic> json,
{i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return PeriodicReminder(
id: serializer.fromJson<int>(json['id']),
frequency: serializer.fromJson<Duration>(json['frequency']),
reminder: serializer.fromJson<String>(json['reminder']),
);
}
@override
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<int>(id),
'frequency': serializer.toJson<Duration>(frequency),
'reminder': serializer.toJson<String>(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<i1.PeriodicReminder> {
final i0.Value<int> id;
final i0.Value<Duration> frequency;
final i0.Value<String> 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<i1.PeriodicReminder> custom({
i0.Expression<int>? id,
i0.Expression<Duration>? frequency,
i0.Expression<String>? reminder,
}) {
return i0.RawValuesInsertable({
if (id != null) 'id': id,
if (frequency != null) 'frequency': frequency,
if (reminder != null) 'reminder': reminder,
});
}
i1.PeriodicRemindersCompanion copyWith(
{i0.Value<int>? id,
i0.Value<Duration>? frequency,
i0.Value<String>? reminder}) {
return i1.PeriodicRemindersCompanion(
id: id ?? this.id,
frequency: frequency ?? this.frequency,
reminder: reminder ?? this.reminder,
);
}
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
if (id.present) {
map['id'] = i0.Variable<int>(id.value);
}
if (frequency.present) {
map['frequency'] =
i0.Variable<Duration>(frequency.value, const i2.DurationType());
}
if (reminder.present) {
map['reminder'] = i0.Variable<String>(reminder.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('PeriodicRemindersCompanion(')
..write('id: $id, ')
..write('frequency: $frequency, ')
..write('reminder: $reminder')
..write(')'))
.toString();
}
}

View File

@ -0,0 +1,19 @@
import 'package:drift/drift.dart';
class DurationType implements CustomSqlType<Duration> {
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';
}

View File

@ -187,6 +187,7 @@ class ShoppingCartsCompanion extends i0.UpdateCompanion<i2.ShoppingCart> {
}
if (entries.present) {
final converter = i2.$ShoppingCartsTable.$converterentries;
map['entries'] = i0.Variable<String>(converter.toSql(entries.value));
}
return map;

View File

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

View File

@ -197,6 +197,13 @@ abstract class Table extends HasResultSet {
@protected
ColumnBuilder<double> 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<T> customType<T extends Object>(CustomSqlType<T> type) =>
_isGenerated();

View File

@ -407,6 +407,8 @@ enum DriftSqlType<T extends Object> implements BaseSqlType<T> {
/// 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<T extends Object>
implements BaseSqlType<T> {
/// Interprets the underlying [fromSql] value from the database driver into