Skip to content

Commit

Permalink
Support dialect-specific types
Browse files Browse the repository at this point in the history
  • Loading branch information
simolus3 committed Jan 10, 2024
1 parent 09c6cf0 commit f25eaf1
Show file tree
Hide file tree
Showing 18 changed files with 287 additions and 56 deletions.
35 changes: 35 additions & 0 deletions docs/lib/snippets/modular/custom_types/type.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// #docregion duration
import 'package:drift/drift.dart';

class DurationType implements CustomSqlType<Duration> {
Expand All @@ -17,3 +18,37 @@ class DurationType implements CustomSqlType<Duration> {
@override
String sqlTypeName(GenerationContext context) => 'interval';
}
// #enddocregion duration

// #docregion fallback
class _FallbackDurationType implements CustomSqlType<Duration> {
const _FallbackDurationType();

@override
String mapToSqlLiteral(Duration dartValue) {
return dartValue.inMicroseconds.toString();
}

@override
Object mapToSqlParameter(Duration dartValue) {
return dartValue.inMicroseconds;
}

@override
Duration read(Object fromSql) {
return Duration(microseconds: fromSql as int);
}

@override
String sqlTypeName(GenerationContext context) {
return 'integer';
}
}
// #enddocregion fallback

const durationType = DialectAwareSqlType<Duration>.via(
fallback: _FallbackDurationType(),
overrides: {
SqlDialect.postgres: DurationType(),
},
);
26 changes: 25 additions & 1 deletion docs/pages/docs/SQL API/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ data:
template: layouts/docs/single
---

{% assign type_snippets = "package:drift_docs/snippets/modular/custom_types/type.dart.excerpt.json" | readString | json_decode %}

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.
Expand Down Expand Up @@ -41,7 +43,7 @@ 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) %}
{% include "blocks/snippet" snippets = type_snippets name = "duration" %}

This type defines the following things:

Expand Down Expand Up @@ -84,3 +86,25 @@ 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 }}

## Dialect awareness

When defining custom types for SQL types only supported on some database management systems, your
database will _only_ work with those database systems. For instance, any table using the `DurationType`
defined above will not work with sqlite3 since it uses an `interval` type interpreted as an integer
by sqlite3 - and the `interval xyz microseconds` syntax is not supported by sqlite3 at all.

Starting with drift 2.15, it is possible to define custom types that behave differently depending on
the dialect used.
This can be used to build polyfills for other database systems. First, consider a custom type storing
durations as integers, similar to what a type converter might do:

{% include "blocks/snippet" snippets = type_snippets name = "fallback" %}

By using a `DialectAwareSqlType`, you can automatically use the `interval` type on PostgreSQL databases
while falling back to an integer type on sqlite3 and other databases:

```dart
Column<Duration> get frequency => customType(durationType)
.clientDefault(() => Duration(minutes: 15))();
```
4 changes: 4 additions & 0 deletions drift/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
## 2.15.0-dev

- Methods in the query builder API now respect custom types.
- Support `DialectAwareSqlType`, custom types that depend on the dialect of the
active database connection. This can be used to use native types not
supported by drift (like UUIDs) on databases that support it while falling
back to a text type on sqlite3.
- Close wasm databases hosted in workers after the last client disconnects.

## 2.14.1
Expand Down
2 changes: 1 addition & 1 deletion drift/lib/drift.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export 'src/runtime/executor/interceptor.dart';
export 'src/runtime/query_builder/query_builder.dart'
hide CaseWhenExpressionWithBase, BaseCaseWhenExpression;
export 'src/runtime/types/converters.dart';
export 'src/runtime/types/mapping.dart' hide BaseSqlType;
export 'src/runtime/types/mapping.dart' hide BaseSqlType, UserDefinedSqlType;
export 'src/utils/lazy_database.dart';

/// A [ListEquality] instance used by generated drift code for the `==` and
Expand Down
2 changes: 1 addition & 1 deletion drift/lib/src/drift_dev_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
export 'dart:typed_data' show Uint8List;

export 'runtime/types/converters.dart' show TypeConverter, JsonTypeConverter2;
export 'runtime/types/mapping.dart' show DriftAny, CustomSqlType;
export 'runtime/types/mapping.dart' show DriftAny, UserDefinedSqlType;
export 'runtime/query_builder/query_builder.dart' show TableInfo;

export 'dsl/dsl.dart'
Expand Down
2 changes: 2 additions & 0 deletions drift/lib/src/dsl/dsl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import 'package:drift/drift.dart';
import 'package:meta/meta.dart';
import 'package:meta/meta_meta.dart';

import '../runtime/types/mapping.dart';

part 'columns.dart';
part 'database.dart';
part 'table.dart';
Expand Down
2 changes: 1 addition & 1 deletion drift/lib/src/dsl/table.dart
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ abstract class Table extends HasResultSet {
/// 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) =>
ColumnBuilder<T> customType<T extends Object>(UserDefinedSqlType<T> type) =>
_isGenerated();
}

Expand Down
3 changes: 2 additions & 1 deletion drift/lib/src/runtime/devtools/shared.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ class TypeDescription {
factory TypeDescription.fromDrift(GenerationContext ctx, BaseSqlType type) {
return switch (type) {
DriftSqlType() => TypeDescription(type: type),
CustomSqlType<Object>() =>
CustomSqlType() ||
DialectAwareSqlType() =>
TypeDescription(customTypeName: type.sqlTypeName(ctx)),
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -549,7 +549,9 @@ class _CastInSqlExpression<D1 extends Object, D2 extends Object>
DriftSqlType.blob => 'BINARY',
DriftSqlType.dateTime => 'DATETIME',
DriftSqlType.any => '',
CustomSqlType() => targetType.sqlTypeName(context),
CustomSqlType() ||
DialectAwareSqlType() =>
targetType.sqlTypeName(context),
};
} else {
typeName = targetType.sqlTypeName(context);
Expand Down
19 changes: 5 additions & 14 deletions drift/lib/src/runtime/query_builder/expressions/variables.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ part of '../query_builder.dart';
final class Variable<T extends Object> extends Expression<T> {
/// The Dart value that will be sent to the database
final T? value;
final CustomSqlType<T>? _customType;
final UserDefinedSqlType<T>? _customType;

// note that we keep the identity hash/equals here because each variable would
// get its own index in sqlite and is thus different.
Expand Down Expand Up @@ -67,12 +67,7 @@ final class Variable<T extends Object> extends Expression<T> {
/// database engine. For instance, a [DateTime] will me mapped to its unix
/// timestamp.
dynamic mapToSimpleValue(GenerationContext context) {
final type = _customType;
if (value != null && type != null) {
return type.mapToSqlParameter(value!);
} else {
return context.typeMapping.mapToSqlVariable(value);
}
return BaseSqlType.mapToSqlParameter<T>(context, _customType, value);
}

@override
Expand Down Expand Up @@ -126,7 +121,7 @@ final class Constant<T extends Object> extends Expression<T> {
/// The value that will be converted to an sql literal.
final T? value;

final CustomSqlType<T>? _customType;
final UserDefinedSqlType<T>? _customType;

/// Constructs a new constant (sql literal) holding the [value].
const Constant(this.value, [this._customType]);
Expand All @@ -142,12 +137,8 @@ final class Constant<T extends Object> extends Expression<T> {

@override
void writeInto(GenerationContext context) {
final type = _customType;
if (value != null && type != null) {
context.buffer.write(type.mapToSqlLiteral(value!));
} else {
context.buffer.write(context.typeMapping.mapToSqlLiteral(value));
}
return context.buffer
.write(BaseSqlType.mapToSqlLiteral(context, _customType, value));
}

@override
Expand Down
123 changes: 117 additions & 6 deletions drift/lib/src/runtime/types/mapping.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ final class SqlTypes {
/// [the documentation]: https://drift.simonbinder.eu/docs/getting-started/advanced_dart_tables/#supported-column-types
final bool storeDateTimesAsText;

final SqlDialect _dialect;
/// The [SqlDialect] to consider when mapping values from and to Dart.
final SqlDialect dialect;

/// Creates an [SqlTypes] mapper from the provided options.
@internal
const SqlTypes(this.storeDateTimesAsText,
[this._dialect = SqlDialect.sqlite]);
const SqlTypes(this.storeDateTimesAsText, [this.dialect = SqlDialect.sqlite]);

/// Maps a Dart object to a (possibly simpler) object that can be used as a
/// parameter in raw sql queries.
Expand Down Expand Up @@ -73,7 +73,7 @@ final class SqlTypes {
}
}

if (dartValue is bool && _dialect == SqlDialect.sqlite) {
if (dartValue is bool && dialect == SqlDialect.sqlite) {
return dartValue ? 1 : 0;
}

Expand All @@ -91,7 +91,7 @@ final class SqlTypes {

// todo: Inline and remove types in the next major drift version
if (dart is bool) {
if (_dialect == SqlDialect.sqlite) {
if (dialect == SqlDialect.sqlite) {
return dart ? '1' : '0';
} else {
return dart ? 'true' : 'false';
Expand Down Expand Up @@ -196,6 +196,7 @@ final class SqlTypes {
},
DriftSqlType.any => DriftAny(sqlValue),
CustomSqlType() => type.read(sqlValue),
DialectAwareSqlType() => type.read(this, sqlValue),
} as T;
}
}
Expand Down Expand Up @@ -273,8 +274,42 @@ final class DriftAny {
sealed class BaseSqlType<T> {
/// Returns a suitable representation of this type in SQL.
String sqlTypeName(GenerationContext context);

static T? read<T extends Object>(
SqlTypes types, BaseSqlType<T> type, Object fromSql) {
return types.read(type, fromSql);
}

static Object? mapToSqlParameter<T extends Object>(
GenerationContext context, BaseSqlType<T>? type, T? value) {
if (value == null) return null;

return switch (type) {
null ||
DriftSqlType<Object>() =>
context.typeMapping.mapToSqlVariable(value),
CustomSqlType<T>() => type.mapToSqlParameter(value),
DialectAwareSqlType<T>() => type.mapToSqlParameter(context, value),
};
}

static String mapToSqlLiteral<T extends Object>(
GenerationContext context, BaseSqlType<T>? type, T? value) {
if (value == null) return 'NULL';

return switch (type) {
null ||
DriftSqlType<Object>() =>
context.typeMapping.mapToSqlLiteral(value),
CustomSqlType<T>() => type.mapToSqlLiteral(value),
DialectAwareSqlType<T>() => type.mapToSqlLiteral(context, value),
};
}
}

@internal
sealed class UserDefinedSqlType<T> implements BaseSqlType<T> {}

/// An enumation of type mappings that are builtin to drift and `drift_dev`.
enum DriftSqlType<T extends Object> implements BaseSqlType<T> {
/// A boolean type, represented as `0` or `1` (int) in SQL.
Expand Down Expand Up @@ -410,7 +445,7 @@ enum DriftSqlType<T extends Object> implements BaseSqlType<T> {
/// 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> {
implements UserDefinedSqlType<T> {
/// Interprets the underlying [fromSql] value from the database driver into
/// the Dart representation [T] of this type.
T read(Object fromSql);
Expand All @@ -423,3 +458,79 @@ abstract interface class CustomSqlType<T extends Object>
/// into SQL queries generated by drift.
String mapToSqlLiteral(T dartValue);
}

/// A [CustomSqlType] with access on the dialect of the database engine when
/// used in queries.
///
/// This can be used to design drift types providing polyfills for types only
/// supported on some databases, for instance by using native `DATE` support
/// on postgres but falling back to a textual representation on sqlite3.
abstract interface class DialectAwareSqlType<T extends Object>
implements UserDefinedSqlType<T> {
/// Creates a [DialectAwareSqlType] that uses the [fallback] type by default,
/// but can apply [overrides] on some database systems.
///
/// For instance, this can be used to create a custom type that stores uuids
/// as `TEXT` on databases with no builtin UUID type, but otherwise uses the
/// native format:
///
/// ```dart
/// class UuidAsTextType implements CustomSqlType<Uuid> { ... }
///
/// const uuidType = DialectAwareSqlType.via(
/// fallback: UuidAsTextType(),
/// overrides: {
/// SqlDialect.postgres: PgTypes.uuid,
/// }
/// );
/// ```
const factory DialectAwareSqlType.via({
required BaseSqlType<T> fallback,
required Map<SqlDialect, BaseSqlType<T>> overrides,
}) = _ByDialectType<T>;

/// Interprets the underlying [fromSql] value from the database driver into
/// the Dart representation [T] of this type.
T read(SqlTypes typeSystem, Object fromSql);

/// Maps the [dartValue] to a value understood by the underlying database
/// driver.
Object mapToSqlParameter(GenerationContext context, T dartValue);

/// Maps the [dartValue] to a SQL snippet that can be embedded as a literal
/// into SQL queries generated by drift.
String mapToSqlLiteral(GenerationContext context, T dartValue);
}

final class _ByDialectType<T extends Object> implements DialectAwareSqlType<T> {
final BaseSqlType<T> fallback;
final Map<SqlDialect, BaseSqlType<T>> overrides;

const _ByDialectType({required this.fallback, required this.overrides});

BaseSqlType<T> _selectType(SqlTypes typeSystem) {
return overrides[typeSystem.dialect] ?? fallback;
}

@override
String mapToSqlLiteral(GenerationContext context, T dartValue) {
return BaseSqlType.mapToSqlLiteral(
context, _selectType(context.typeMapping), dartValue);
}

@override
Object mapToSqlParameter(GenerationContext context, T dartValue) {
return BaseSqlType.mapToSqlParameter(
context, _selectType(context.typeMapping), dartValue)!;
}

@override
T read(SqlTypes typeSystem, Object fromSql) {
return BaseSqlType.read(typeSystem, _selectType(typeSystem), fromSql)!;
}

@override
String sqlTypeName(GenerationContext context) {
return _selectType(context.typeMapping).sqlTypeName(context);
}
}
6 changes: 6 additions & 0 deletions drift/test/database/statements/schema_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,12 @@ void main() {
test('creates tables with custom types', () async {
await db.createMigrator().createTable(db.withCustomType);

verify(mockExecutor.runCustom(
'CREATE TABLE IF NOT EXISTS "with_custom_type" ("id" text NOT NULL);',
[]));

when(mockExecutor.dialect).thenReturn(SqlDialect.postgres);
await db.createMigrator().createTable(db.withCustomType);
verify(mockExecutor.runCustom(
'CREATE TABLE IF NOT EXISTS "with_custom_type" ("id" uuid NOT NULL);',
[]));
Expand Down
Loading

0 comments on commit f25eaf1

Please sign in to comment.