Custom SQL types
Drift's core library is written with sqlite3 as a primary target. This is reflected in the SQL 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
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.
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.
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:
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';
}
This type defines the following things:
- When
Duration
values are mapped to SQL literals (for instance, because they're used inConstant
s), we represent them asinterval '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 inread
also just casts the value. - The name to use in
CREATE TABLE
statements and casts isinterval
.
Using custom types¶
In Dart¶
To define a custom type on a Dart table, use the customType
column builder method with the type:
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()();
}
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 syntax may be used to define the custom type:
import 'type.dart';
CREATE TABLE periodic_reminders (
id INTEGER NOT NULL PRIMARY KEY,
frequency `const DurationType()` NOT NULL,
reminder TEXT NOT NULL
);
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!
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:
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';
}
}
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: