Merge pull request #2669 from simolus3/custom-types

Support custom SQL types
This commit is contained in:
Simon Binder 2023-10-07 22:59:18 +02:00 committed by GitHub
commit e1b5da3160
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
67 changed files with 1861 additions and 318 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

@ -1,6 +1,8 @@
## 2.13.0-dev
- Add APIs to setup Wasm databases with custom drift workers.
- Add support for [custom types](https://drift.simonbinder.eu/docs/sql-api/types/),
which are useful when extending drift to support other database engines.
- Add `Expression.and` and `Expression.or` to create disjunctions and conjunctions
of sub-predicates.
- Step-by-step migrations now save the intermediate schema version after each step.

View File

@ -691,17 +691,10 @@ abstract class _$Database extends GeneratedDatabase {
$TodoCategoryItemCountView(this);
late final $TodoItemWithCategoryNameViewView customViewName =
$TodoItemWithCategoryNameViewView(this);
late final Index itemTitle =
Index('item_title', 'CREATE INDEX item_title ON todo_items (title)');
@override
Iterable<TableInfo<Table, Object?>> get allTables =>
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
@override
List<DatabaseSchemaEntity> get allSchemaEntities => [
todoCategories,
todoItems,
todoCategoryItemCount,
customViewName,
itemTitle
];
List<DatabaseSchemaEntity> get allSchemaEntities =>
[todoCategories, todoItems, todoCategoryItemCount, customViewName];
}

View File

@ -18,7 +18,7 @@ export 'src/runtime/executor/executor.dart';
export 'src/runtime/query_builder/query_builder.dart'
hide CaseWhenExpressionWithBase, BaseCaseWhenExpression;
export 'src/runtime/types/converters.dart';
export 'src/runtime/types/mapping.dart';
export 'src/runtime/types/mapping.dart' hide BaseSqlType;
export 'src/utils/lazy_database.dart';
/// A [ListEquality] instance used by generated drift code for the `==` and

View File

@ -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;
export 'runtime/types/mapping.dart' show DriftAny, CustomSqlType;
export 'runtime/query_builder/query_builder.dart' show TableInfo;
export 'dsl/dsl.dart'

View File

@ -196,6 +196,17 @@ 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();
}
/// Subclasses represent a view in a database generated by drift.

View File

@ -33,6 +33,7 @@ const Expression<DateTime> currentDateAndTime = _DependingOnDateTimeExpression(
'strftime',
[Constant('%s'), _currentTimestampLiteral],
),
DriftSqlType.dateTime,
),
);
@ -341,7 +342,7 @@ class _DependingOnDateTimeExpression<D extends Object> extends Expression<D> {
/// For another explanation of modifiers, see the [sqlite3 docs].
///
/// [sqlite3 docs]: https://sqlite.org/lang_datefunc.html#modifiers
class DateTimeModifier extends Constant<String> {
final class DateTimeModifier extends Constant<String> {
const DateTimeModifier._(super.value);
/// Adds or subtracts [days] calendar days from the date time value.

View File

@ -90,8 +90,13 @@ abstract class Expression<D extends Object> implements FunctionParameter {
/// Note that this does not do a meaningful conversion for drift-only types
/// like `bool` or `DateTime`. Both would simply generate a `CAST AS INT`
/// expression.
Expression<D2> cast<D2 extends Object>() {
return _CastInSqlExpression<D, D2>(this);
///
/// The optional [type] parameter can be used to specify the SQL type to cast
/// to. This is mainly useful for [CustomSqlType]s. For types supported by
/// drift, [DriftSqlType.forType] will be used as a default.
Expression<D2> cast<D2 extends Object>([BaseSqlType<D2>? type]) {
return _CastInSqlExpression<D, D2>(
this, type ?? DriftSqlType.forType<D2>());
}
/// Generates an `IS` expression in SQL, comparing this expression with the
@ -269,8 +274,11 @@ abstract class Expression<D extends Object> implements FunctionParameter {
inner.writeAroundPrecedence(ctx, precedence);
}
/// The supported [DriftSqlType] backing this expression.
DriftSqlType<D> get driftSqlType => DriftSqlType.forType();
/// The [BaseSqlType] backing this expression.
///
/// This is a recognized [DriftSqlType] for all expressions for which a custom
/// type has not explicitly been set.
BaseSqlType<D> get driftSqlType => DriftSqlType.forType();
/// Chains all [predicates] together into a single expression that will
/// evaluate to `true` iff any of the [predicates] evaluates to `true`.
@ -503,16 +511,20 @@ class _DartCastExpression<D1 extends Object, D2 extends Object>
class _CastInSqlExpression<D1 extends Object, D2 extends Object>
extends Expression<D2> {
final Expression<D1> inner;
final BaseSqlType<D2> targetType;
@override
Precedence get precedence => Precedence.primary;
const _CastInSqlExpression(this.inner);
@override
BaseSqlType<D2> get driftSqlType => targetType;
const _CastInSqlExpression(this.inner, this.targetType);
@override
void writeInto(GenerationContext context) {
final type = DriftSqlType.forType<D2>();
if (type == DriftSqlType.any) {
// ignore: unrelated_type_equality_checks
if (targetType == DriftSqlType.any) {
inner.writeInto(context); // No need to cast
}
@ -523,7 +535,7 @@ class _CastInSqlExpression<D1 extends Object, D2 extends Object>
// ones used in a create table statement.
// ignore: unnecessary_cast
typeName = switch (type as DriftSqlType<Object>) {
typeName = switch (targetType) {
DriftSqlType.int ||
DriftSqlType.bigInt ||
DriftSqlType.bool =>
@ -533,9 +545,10 @@ class _CastInSqlExpression<D1 extends Object, D2 extends Object>
DriftSqlType.blob => 'BINARY',
DriftSqlType.dateTime => 'DATETIME',
DriftSqlType.any => '',
CustomSqlType() => targetType.sqlTypeName(context),
};
} else {
typeName = type.sqlTypeName(context);
typeName = targetType.sqlTypeName(context);
}
context.buffer.write('CAST(');

View File

@ -5,9 +5,10 @@ part of '../query_builder.dart';
/// An expression that represents the value of a dart object encoded to sql
/// using prepared statements.
class Variable<T extends Object> extends Expression<T> {
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;
// note that we keep the identity hash/equals here because each variable would
// get its own index in sqlite and is thus different.
@ -18,8 +19,14 @@ class Variable<T extends Object> extends Expression<T> {
@override
int get hashCode => value.hashCode;
@override
BaseSqlType<T> get driftSqlType => _customType ?? super.driftSqlType;
/// Constructs a new variable from the [value].
const Variable(this.value);
///
/// For variables of [CustomSqlType]s, the `type` can also be provided as a
/// parameter to control how the value is mapped to SQL.
const Variable(this.value, [this._customType]);
/// Creates a variable that holds the specified boolean.
static Variable<bool> withBool(bool value) {
@ -60,7 +67,12 @@ 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) {
return context.typeMapping.mapToSqlVariable(value);
final type = _customType;
if (value != null && type != null) {
return type.mapToSqlParameter(value!);
} else {
return context.typeMapping.mapToSqlVariable(value);
}
}
@override
@ -110,22 +122,32 @@ class Variable<T extends Object> extends Expression<T> {
/// An expression that represents the value of a dart object encoded to sql
/// by writing them into the sql statements. For most cases, consider using
/// [Variable] instead.
class Constant<T extends Object> extends Expression<T> {
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;
/// Constructs a new constant (sql literal) holding the [value].
const Constant(this.value);
const Constant(this.value, [this._customType]);
@override
Precedence get precedence => Precedence.primary;
/// The value that will be converted to an sql literal.
final T? value;
@override
BaseSqlType<T> get driftSqlType => _customType ?? super.driftSqlType;
@override
bool get isLiteral => true;
@override
void writeInto(GenerationContext context) {
context.buffer.write(context.typeMapping.mapToSqlLiteral(value));
final type = _customType;
if (value != null && type != null) {
context.buffer.write(type.mapToSqlLiteral(value!));
} else {
context.buffer.write(context.typeMapping.mapToSqlLiteral(value));
}
}
@override

View File

@ -54,7 +54,7 @@ class GeneratedColumn<T extends Object> extends Column<T> {
final VerificationResult Function(T?, VerificationMeta)? additionalChecks;
/// The sql type to use for this column.
final DriftSqlType<T> type;
final BaseSqlType<T> type;
/// If this column is generated (that is, it is a SQL expression of other)
/// columns, contains information about how to generate this column.
@ -70,6 +70,9 @@ class GeneratedColumn<T extends Object> extends Column<T> {
@override
String get name => $name;
@override
BaseSqlType<T> get driftSqlType => type;
/// Used by generated code.
GeneratedColumn(
this.$name,
@ -294,7 +297,7 @@ class GeneratedColumnWithTypeConverter<D, S extends Object>
String tableName,
bool nullable,
S? Function()? clientDefault,
DriftSqlType<S> type,
BaseSqlType<S> type,
void Function(GenerationContext)? defaultConstraints,
String? customConstraints,
Expression<S>? defaultValue,

View File

@ -81,7 +81,17 @@ class QueryRow {
/// support non-nullable types.
T read<T>(String key) {
final type = DriftSqlType.forNullableType<T>();
return _db.typeMapping.read(type, data[key]) as T;
return readNullableWithType(type, key) as T;
}
/// Interprets the column named [key] under the known drift type [type].
///
/// Like [read], except that the [type] is fixed and not inferred from the
/// type parameter [T]. Also, this method does not support nullable values -
/// use [readNullableWithType] if needed.
@optionalTypeArgs
T readWithType<T extends Object>(BaseSqlType<T> type, String key) {
return _db.typeMapping.read(type, data[key])!;
}
/// Reads a nullable value from this row.
@ -90,7 +100,7 @@ class QueryRow {
/// drift (e.g. booleans, strings, numbers, dates, `Uint8List`s).
T? readNullable<T extends Object>(String key) {
final type = DriftSqlType.forType<T>();
return _db.typeMapping.read(type, data[key]);
return readNullableWithType(type, key);
}
/// Interprets the column named [key] under the known drift type [type].
@ -98,7 +108,7 @@ class QueryRow {
/// Like [readNullable], except that the [type] is fixed and not inferred from
/// the type parameter [T].
@optionalTypeArgs
T? readNullableWithType<T extends Object>(DriftSqlType<T> type, String key) {
T? readNullableWithType<T extends Object>(BaseSqlType<T> type, String key) {
return _db.typeMapping.read(type, data[key]);
}

View File

@ -10,8 +10,7 @@ import '../query_builder/query_builder.dart';
/// Database-specific helper methods mapping Dart values from and to SQL
/// variables or literals.
@sealed
class SqlTypes {
final class SqlTypes {
// Stolen from DateTime._parseFormat
static final RegExp _timeZoneInDateTime =
RegExp(r' ?([-+])(\d\d)(?::?(\d\d))?$');
@ -127,76 +126,77 @@ class SqlTypes {
'Must be null, bool, String, int, DateTime, Uint8List or double');
}
/// Maps a raw [sqlValue] to Dart given its sql [type].
T? read<T extends Object>(DriftSqlType<T> type, Object? sqlValue) {
DateTime _readDateTime(Object sqlValue) {
if (storeDateTimesAsText) {
final rawValue = read(DriftSqlType.string, sqlValue)!;
DateTime result;
// We store date times like this:
//
// - if it's in UTC, we call [DateTime.toIso8601String], so there's a
// trailing `Z`. We can just use [DateTime.parse] and get an utc
// datetime back.
// - for local date times, we append the time zone offset, e.g.
// `+02:00`. [DateTime.parse] respects this UTC offset and returns
// the correct date, but it returns it in UTC. Since we only use
// this format for local times, we need to transform it back to
// local.
//
// Additionally, complex date time expressions are wrapped in a
// `datetime` sqlite call, which doesn't append a `Z` or a time zone
// offset. As sqlite3 always uses UTC for these computations
// internally, we'll return a UTC datetime as well.
if (_timeZoneInDateTime.hasMatch(rawValue)) {
// Case 2: Explicit time zone offset given, we do this for local
// dates.
result = DateTime.parse(rawValue).toLocal();
} else if (rawValue.endsWith('Z')) {
// Case 1: Date time in UTC, [DateTime.parse] will do the right
// thing.
result = DateTime.parse(rawValue);
} else {
// Result from complex date tmie transformation. Interpret as UTC,
// which is what sqlite3 does by default.
result = DateTime.parse('${rawValue}Z');
}
return result;
} else {
final unixSeconds = read(DriftSqlType.int, sqlValue)!;
return DateTime.fromMillisecondsSinceEpoch(unixSeconds * 1000);
}
}
/// Maps a raw [sqlValue] to Dart given its sql [type] (typically a
/// [DriftSqlType]).
T? read<T extends Object>(BaseSqlType<T> type, Object? sqlValue) {
if (sqlValue == null) return null;
// ignore: unnecessary_cast
switch (type as DriftSqlType<Object>) {
case DriftSqlType.bool:
return (sqlValue != 0 && sqlValue != false) as T;
case DriftSqlType.string:
return sqlValue.toString() as T;
case DriftSqlType.bigInt:
if (sqlValue is BigInt) return sqlValue as T?;
if (sqlValue is int) return BigInt.from(sqlValue) as T;
return BigInt.parse(sqlValue.toString()) as T;
case DriftSqlType.int:
if (sqlValue is int) return sqlValue as T;
if (sqlValue is BigInt) return sqlValue.toInt() as T;
return int.parse(sqlValue.toString()) as T;
case DriftSqlType.dateTime:
if (storeDateTimesAsText) {
final rawValue = read(DriftSqlType.string, sqlValue)!;
DateTime result;
// We store date times like this:
//
// - if it's in UTC, we call [DateTime.toIso8601String], so there's a
// trailing `Z`. We can just use [DateTime.parse] and get an utc
// datetime back.
// - for local date times, we append the time zone offset, e.g.
// `+02:00`. [DateTime.parse] respects this UTC offset and returns
// the correct date, but it returns it in UTC. Since we only use
// this format for local times, we need to transform it back to
// local.
//
// Additionally, complex date time expressions are wrapped in a
// `datetime` sqlite call, which doesn't append a `Z` or a time zone
// offset. As sqlite3 always uses UTC for these computations
// internally, we'll return a UTC datetime as well.
if (_timeZoneInDateTime.hasMatch(rawValue)) {
// Case 2: Explicit time zone offset given, we do this for local
// dates.
result = DateTime.parse(rawValue).toLocal();
} else if (rawValue.endsWith('Z')) {
// Case 1: Date time in UTC, [DateTime.parse] will do the right
// thing.
result = DateTime.parse(rawValue);
} else {
// Result from complex date tmie transformation. Interpret as UTC,
// which is what sqlite3 does by default.
result = DateTime.parse('${rawValue}Z');
}
return result as T;
} else {
final unixSeconds = read(DriftSqlType.int, sqlValue)!;
return DateTime.fromMillisecondsSinceEpoch(unixSeconds * 1000) as T;
}
case DriftSqlType.blob:
if (sqlValue is String) {
final list = sqlValue.codeUnits;
return Uint8List.fromList(list) as T;
}
return sqlValue as T;
case DriftSqlType.double:
if (sqlValue case final BigInt bi) return bi.toDouble() as T;
return (sqlValue as num?)?.toDouble() as T;
case DriftSqlType.any:
return DriftAny(sqlValue) as T;
}
return switch (type) {
DriftSqlType.bool => (sqlValue != 0 && sqlValue != false),
DriftSqlType.string => sqlValue.toString(),
DriftSqlType.bigInt => switch (sqlValue) {
BigInt() => sqlValue,
int() => BigInt.from(sqlValue),
_ => BigInt.parse(sqlValue.toString()),
},
DriftSqlType.int => switch (sqlValue) {
int() => sqlValue,
BigInt() => sqlValue.toInt(),
_ => int.parse(sqlValue.toString()),
},
DriftSqlType.dateTime => _readDateTime(sqlValue),
DriftSqlType.blob => switch (sqlValue) {
String() => Uint8List.fromList(sqlValue.codeUnits),
_ => sqlValue,
},
DriftSqlType.double => switch (sqlValue) {
BigInt() => sqlValue.toDouble(),
_ => (sqlValue as num).toDouble(),
},
DriftSqlType.any => DriftAny(sqlValue),
CustomSqlType() => type.read(sqlValue),
} as T;
}
}
@ -212,8 +212,7 @@ class SqlTypes {
/// column with an `ANY` type.
///
/// [STRICT tables]: https://www.sqlite.org/stricttables.html
@sealed
class DriftAny {
final class DriftAny {
/// The direct, unmodified SQL value being wrapped by this [DriftAny]
/// instance.
///
@ -249,7 +248,7 @@ class DriftAny {
/// [DatabaseConnectionUser.typeMapping].
///
/// [as text]: https://drift.simonbinder.eu/docs/getting-started/advanced_dart_tables/#datetime-options
T readAs<T extends Object>(DriftSqlType<T> type, SqlTypes types) {
T readAs<T extends Object>(BaseSqlType<T> type, SqlTypes types) {
return types.read<T>(type, rawSqlValue)!;
}
@ -268,19 +267,16 @@ class DriftAny {
}
}
/// In [DriftSqlType.forNullableType], we need to do an `is` check over
/// `DriftSqlType<T>` with a potentially nullable `T`. Since `DriftSqlType` is
/// defined with a non-nullable `T`, this is illegal.
/// The non-nullable upper bound in [DriftSqlType] is generally useful, for
/// instance because it works well with [SqlTypes.read] which can then have a
/// sound nullable return type.
///
/// As a hack, we define this base class that doesn't have this restriction and
/// use this one for type checks.
abstract class _InternalDriftSqlType<T> {}
/// The superclass for SQL types, whether built-in to drift ([DriftSqlType]) or
/// provided by the user through [CustomSqlType]s.
@internal
sealed class BaseSqlType<T> {
/// Returns a suitable representation of this type in SQL.
String sqlTypeName(GenerationContext context);
}
/// An enumation of type mappings that are builtin to drift and `drift_dev`.
enum DriftSqlType<T extends Object> implements _InternalDriftSqlType<T> {
enum DriftSqlType<T extends Object> implements BaseSqlType<T> {
/// A boolean type, represented as `0` or `1` (int) in SQL.
bool<core.bool>(),
@ -316,7 +312,7 @@ enum DriftSqlType<T extends Object> implements _InternalDriftSqlType<T> {
/// [STRICT tables]: https://www.sqlite.org/stricttables.html
any<DriftAny>();
/// Returns a suitable representation of this type in SQL.
@override
String sqlTypeName(GenerationContext context) {
final dialect = context.dialect;
@ -390,7 +386,7 @@ enum DriftSqlType<T extends Object> implements _InternalDriftSqlType<T> {
// typecheck where that doesn't work (which can be the case for complex
// type like `forNullableType<FutureOr<int?>>`).
final type = _dartToDrift[Dart] ??
values.whereType<_InternalDriftSqlType<Dart>>().singleOrNull;
values.whereType<BaseSqlType<Dart>>().singleOrNull;
if (type == null) {
throw ArgumentError('Could not find a matching SQL type for $Dart');
@ -399,3 +395,31 @@ enum DriftSqlType<T extends Object> implements _InternalDriftSqlType<T> {
return type as DriftSqlType;
}
}
/// Interface for a custom SQL type.
///
/// Being designed with sqlite3 as its primary database engine, drift lacks
/// builtin support for the rich type system found in more complex database
/// systems like postgres. By providing the [CustomSqlType] interface, drift can
/// be extended to support any database type by customizing the way these types
/// are mapped from and to the database.
///
/// 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
/// the Dart representation [T] of this type.
T read(Object fromSql);
/// Maps the [dartValue] to a value understood by the underlying database
/// driver.
Object mapToSqlParameter(T dartValue);
/// Maps the [dartValue] to a SQL snippet that can be embedded as a literal
/// into SQL queries generated by drift.
String mapToSqlLiteral(T dartValue);
}

View File

@ -109,6 +109,14 @@ 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" uuid NOT NULL);',
[]));
});
test('creates views through create()', () async {
await db.createMigrator().create(db.categoryTodoCountView);

View File

@ -0,0 +1,64 @@
import 'package:drift/drift.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
import 'package:uuid/uuid.dart';
import '../../generated/todos.dart';
import '../../test_utils/test_utils.dart';
void main() {
final uuid = Uuid().v4obj();
group('in expression', () {
test('variable', () {
final c = Variable<UuidValue>(uuid, const UuidType());
expect(c.driftSqlType, isA<UuidType>());
expect(c, generates('?', [uuid]));
});
test('constant', () {
final c = Constant<UuidValue>(uuid, const UuidType());
expect(c.driftSqlType, isA<UuidType>());
expect(c, generates("'$uuid'"));
});
test('cast', () {
final cast = Variable('foo').cast<UuidValue>(const UuidType());
expect(cast.driftSqlType, isA<UuidType>());
expect(cast, generates('CAST(? AS uuid)', ['foo']));
});
});
test('for inserts', () async {
final executor = MockExecutor();
final database = TodoDb(executor);
addTearDown(database.close);
final uuid = Uuid().v4obj();
await database
.into(database.withCustomType)
.insert(WithCustomTypeCompanion.insert(id: uuid));
verify(executor
.runInsert('INSERT INTO "with_custom_type" ("id") VALUES (?)', [uuid]));
});
test('for selects', () async {
final executor = MockExecutor();
final database = TodoDb(executor);
addTearDown(database.close);
final uuid = Uuid().v4obj();
when(executor.runSelect(any, any)).thenAnswer((_) {
return Future.value([
{'id': uuid}
]);
});
final row = await database.withCustomType.all().getSingle();
expect(row.id, uuid);
});
}

View File

@ -797,10 +797,12 @@ class ConfigCompanion extends UpdateCompanion<Config> {
}
if (syncState.present) {
final converter = ConfigTable.$convertersyncStaten;
map['sync_state'] = Variable<int>(converter.toSql(syncState.value));
}
if (syncStateImplicit.present) {
final converter = ConfigTable.$convertersyncStateImplicitn;
map['sync_state_implicit'] =
Variable<int>(converter.toSql(syncStateImplicit.value));
}

View File

@ -180,6 +180,32 @@ abstract class TodoWithCategoryView extends View {
.join([innerJoin(categories, categories.id.equalsExp(todos.category))]);
}
class WithCustomType extends Table {
Column<UuidValue> get id => customType(const UuidType())();
}
class UuidType implements CustomSqlType<UuidValue> {
const UuidType();
@override
String mapToSqlLiteral(UuidValue dartValue) {
return "'$dartValue'";
}
@override
Object mapToSqlParameter(UuidValue dartValue) {
return dartValue;
}
@override
UuidValue read(Object fromSql) {
return fromSql as UuidValue;
}
@override
String sqlTypeName(GenerationContext context) => 'uuid';
}
@DriftDatabase(
tables: [
TodosTable,
@ -188,6 +214,7 @@ abstract class TodoWithCategoryView extends View {
SharedTodos,
TableWithoutPK,
PureDefaults,
WithCustomType,
],
views: [
CategoryTodoCountView,

View File

@ -247,6 +247,7 @@ class CategoriesCompanion extends UpdateCompanion<Category> {
}
if (priority.present) {
final converter = $CategoriesTable.$converterpriority;
map['priority'] = Variable<int>(converter.toSql(priority.value));
}
return map;
@ -598,6 +599,7 @@ class TodosTableCompanion extends UpdateCompanion<TodoEntry> {
}
if (status.present) {
final converter = $TodosTableTable.$converterstatusn;
map['status'] = Variable<String>(converter.toSql(status.value));
}
return map;
@ -1269,6 +1271,7 @@ class TableWithoutPKCompanion extends UpdateCompanion<CustomRowClass> {
}
if (custom.present) {
final converter = $TableWithoutPKTable.$convertercustom;
map['custom'] = Variable<String>(converter.toSql(custom.value));
}
if (rowid.present) {
@ -1455,6 +1458,7 @@ class PureDefaultsCompanion extends UpdateCompanion<PureDefault> {
final map = <String, Expression>{};
if (txt.present) {
final converter = $PureDefaultsTable.$convertertxtn;
map['insert'] = Variable<String>(converter.toSql(txt.value));
}
if (rowid.present) {
@ -1473,6 +1477,160 @@ class PureDefaultsCompanion extends UpdateCompanion<PureDefault> {
}
}
class $WithCustomTypeTable extends WithCustomType
with TableInfo<$WithCustomTypeTable, WithCustomTypeData> {
@override
final GeneratedDatabase attachedDatabase;
final String? _alias;
$WithCustomTypeTable(this.attachedDatabase, [this._alias]);
static const VerificationMeta _idMeta = const VerificationMeta('id');
@override
late final GeneratedColumn<UuidValue> id = GeneratedColumn<UuidValue>(
'id', aliasedName, false,
type: const UuidType(), requiredDuringInsert: true);
@override
List<GeneratedColumn> get $columns => [id];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'with_custom_type';
@override
VerificationContext validateIntegrity(Insertable<WithCustomTypeData> instance,
{bool isInserting = false}) {
final context = VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('id')) {
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
} else if (isInserting) {
context.missing(_idMeta);
}
return context;
}
@override
Set<GeneratedColumn> get $primaryKey => const {};
@override
WithCustomTypeData map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return WithCustomTypeData(
id: attachedDatabase.typeMapping
.read(const UuidType(), data['${effectivePrefix}id'])!,
);
}
@override
$WithCustomTypeTable createAlias(String alias) {
return $WithCustomTypeTable(attachedDatabase, alias);
}
}
class WithCustomTypeData extends DataClass
implements Insertable<WithCustomTypeData> {
final UuidValue id;
const WithCustomTypeData({required this.id});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
map['id'] = Variable<UuidValue>(id);
return map;
}
WithCustomTypeCompanion toCompanion(bool nullToAbsent) {
return WithCustomTypeCompanion(
id: Value(id),
);
}
factory WithCustomTypeData.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return WithCustomTypeData(
id: serializer.fromJson<UuidValue>(json['id']),
);
}
factory WithCustomTypeData.fromJsonString(String encodedJson,
{ValueSerializer? serializer}) =>
WithCustomTypeData.fromJson(
DataClass.parseJson(encodedJson) as Map<String, dynamic>,
serializer: serializer);
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<UuidValue>(id),
};
}
WithCustomTypeData copyWith({UuidValue? id}) => WithCustomTypeData(
id: id ?? this.id,
);
@override
String toString() {
return (StringBuffer('WithCustomTypeData(')
..write('id: $id')
..write(')'))
.toString();
}
@override
int get hashCode => id.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is WithCustomTypeData && other.id == this.id);
}
class WithCustomTypeCompanion extends UpdateCompanion<WithCustomTypeData> {
final Value<UuidValue> id;
final Value<int> rowid;
const WithCustomTypeCompanion({
this.id = const Value.absent(),
this.rowid = const Value.absent(),
});
WithCustomTypeCompanion.insert({
required UuidValue id,
this.rowid = const Value.absent(),
}) : id = Value(id);
static Insertable<WithCustomTypeData> custom({
Expression<UuidValue>? id,
Expression<int>? rowid,
}) {
return RawValuesInsertable({
if (id != null) 'id': id,
if (rowid != null) 'rowid': rowid,
});
}
WithCustomTypeCompanion copyWith({Value<UuidValue>? id, Value<int>? rowid}) {
return WithCustomTypeCompanion(
id: id ?? this.id,
rowid: rowid ?? this.rowid,
);
}
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
if (id.present) {
map['id'] = Variable<UuidValue>(id.value, const UuidType());
}
if (rowid.present) {
map['rowid'] = Variable<int>(rowid.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('WithCustomTypeCompanion(')
..write('id: $id, ')
..write('rowid: $rowid')
..write(')'))
.toString();
}
}
class CategoryTodoCountViewData extends DataClass {
final int? categoryId;
final String? description;
@ -1703,6 +1861,7 @@ abstract class _$TodoDb extends GeneratedDatabase {
late final $SharedTodosTable sharedTodos = $SharedTodosTable(this);
late final $TableWithoutPKTable tableWithoutPK = $TableWithoutPKTable(this);
late final $PureDefaultsTable pureDefaults = $PureDefaultsTable(this);
late final $WithCustomTypeTable withCustomType = $WithCustomTypeTable(this);
late final $CategoryTodoCountViewView categoryTodoCountView =
$CategoryTodoCountViewView(this);
late final $TodoWithCategoryViewView todoWithCategoryView =
@ -1787,6 +1946,7 @@ abstract class _$TodoDb extends GeneratedDatabase {
sharedTodos,
tableWithoutPK,
pureDefaults,
withCustomType,
categoryTodoCountView,
todoWithCategoryView
];

View File

@ -22,6 +22,7 @@ const String _startBool = 'boolean';
const String _startDateTime = 'dateTime';
const String _startBlob = 'blob';
const String _startReal = 'real';
const String _startCustom = 'customType';
const Set<String> _starters = {
_startInt,
@ -33,6 +34,7 @@ const Set<String> _starters = {
_startDateTime,
_startBlob,
_startReal,
_startCustom,
};
const String _methodNamed = 'named';
@ -337,19 +339,40 @@ class ColumnParser {
remainingExpr = inner;
}
_resolver.resolver.driver.options.caseFromDartToSql;
final sqlName = foundExplicitName ??
_resolver.resolver.driver.options.caseFromDartToSql
.apply(getter.name.lexeme);
final sqlType = _startMethodToColumnType(foundStartMethod);
ColumnType columnType;
final helper = await _resolver.resolver.driver.loadKnownTypes();
if (foundStartMethod == _startCustom) {
final expression = remainingExpr.argumentList.arguments.single;
final custom = readCustomType(
element.library!,
expression,
helper,
(message) => _resolver.reportError(
DriftAnalysisError.inDartAst(element, mappedAs!, message),
),
);
columnType = custom != null
? ColumnType.custom(custom)
// Fallback if we fail to read the custom type - we'll also emit an
// error int that case.
: ColumnType.drift(DriftSqlType.any);
} else {
columnType =
ColumnType.drift(_startMethodToBuiltinColumnType(foundStartMethod));
}
AppliedTypeConverter? converter;
if (mappedAs != null) {
converter = readTypeConverter(
element.library!,
mappedAs,
sqlType,
columnType,
nullable,
(message) => _resolver.reportError(
DriftAnalysisError.inDartAst(element, mappedAs!, message)),
@ -437,7 +460,7 @@ class ColumnParser {
return PendingColumnInformation(
DriftColumn(
sqlType: sqlType,
sqlType: columnType,
nullable: nullable,
nameInSql: sqlName,
nameInDart: element.name!,
@ -454,7 +477,7 @@ class ColumnParser {
);
}
DriftSqlType _startMethodToColumnType(String name) {
DriftSqlType _startMethodToBuiltinColumnType(String name) {
return const {
_startBool: DriftSqlType.bool,
_startString: DriftSqlType.string,

View File

@ -27,6 +27,7 @@ class KnownDriftTypes {
final InterfaceType tableInfoType;
final InterfaceType driftDatabase;
final InterfaceType driftAccessor;
final InterfaceElement customSqlType;
final InterfaceElement typeConverter;
final InterfaceElement jsonTypeConverter;
final InterfaceType driftAny;
@ -39,6 +40,7 @@ class KnownDriftTypes {
this.tableIndexType,
this.viewType,
this.tableInfoType,
this.customSqlType,
this.typeConverter,
this.jsonTypeConverter,
this.driftDatabase,
@ -62,6 +64,7 @@ class KnownDriftTypes {
(exportNamespace.get('TableIndex') as InterfaceElement).thisType,
(exportNamespace.get('View') as InterfaceElement).thisType,
(exportNamespace.get('TableInfo') as InterfaceElement).thisType,
exportNamespace.get('CustomSqlType') as InterfaceElement,
exportNamespace.get('TypeConverter') as InterfaceElement,
exportNamespace.get('JsonTypeConverter2') as InterfaceElement,
dbElement.defaultInstantiation,
@ -81,6 +84,10 @@ class KnownDriftTypes {
return type.asInstanceOf(typeConverter);
}
InterfaceType? asCustomType(DartType type) {
return type.asInstanceOf(customSqlType);
}
/// Converts the given Dart [type] into an instantiation of the
/// `JsonTypeConverter` class from drift.
///

View File

@ -275,7 +275,7 @@ class DartViewResolver extends LocalElementResolver<DiscoveredDartView> {
columns.add(DriftColumn(
declaration: DriftDeclaration.dartElement(getter),
sqlType: sqlType,
sqlType: ColumnType.drift(sqlType),
nameInDart: getter.name,
nameInSql: ReCase(getter.name).snakeCase,
nullable: true,

View File

@ -3,7 +3,6 @@ import 'package:analyzer/dart/element/nullability_suffix.dart';
import 'package:collection/collection.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:drift/drift.dart';
import 'package:sqlparser/sqlparser.dart';
import 'package:sqlparser/utils/find_referenced_tables.dart';
@ -22,8 +21,33 @@ abstract class DriftElementResolver<T extends DiscoveredElement>
DriftElementResolver(
super.file, super.discovered, super.resolver, super.state);
Future<CustomColumnType?> resolveCustomColumnType(
InlineDartToken type) async {
dart.Expression expression;
try {
expression = await resolver.driver.backend.resolveExpression(
file.ownUri,
type.dartCode,
file.discovery!.importDependencies
.map((e) => e.uri.toString())
.where((e) => e.endsWith('.dart')),
);
} on CannotReadExpressionException catch (e) {
reportError(DriftAnalysisError.inDriftFile(type, e.msg));
return null;
}
final knownTypes = await resolver.driver.loadKnownTypes();
return readCustomType(
knownTypes.helperLibrary,
expression,
knownTypes,
(msg) => reportError(DriftAnalysisError.inDriftFile(type, msg)),
);
}
Future<AppliedTypeConverter?> typeConverterFromMappedBy(
DriftSqlType sqlType, bool nullable, MappedBy mapper) async {
ColumnType sqlType, bool nullable, MappedBy mapper) async {
final code = mapper.mapper.dartCode;
dart.Expression expression;

View File

@ -73,14 +73,17 @@ class TypeMapping {
}
ResolvedType _columnType(DriftColumn column) {
final type =
_driftTypeToParser(column.sqlType).withNullable(column.nullable);
var type = _driftTypeToParser(column.sqlType.builtin)
.withNullable(column.nullable);
if (column.typeConverter case final AppliedTypeConverter c) {
return type.addHint(TypeConverterHint(c));
} else {
return type;
if (column.sqlType.isCustom) {
type = type.addHint(CustomTypeHint(column.sqlType.custom!));
}
if (column.typeConverter case AppliedTypeConverter c) {
type = type.addHint(TypeConverterHint(c));
}
return type;
}
ResolvedType _driftTypeToParser(DriftSqlType type) {
@ -103,11 +106,7 @@ class TypeMapping {
};
}
DriftSqlType sqlTypeToDrift(ResolvedType? type) {
if (type == null) {
return DriftSqlType.string;
}
DriftSqlType _toDefaultType(ResolvedType type) {
switch (type.type) {
case null:
case BasicType.nullType:
@ -137,6 +136,19 @@ class TypeMapping {
return DriftSqlType.any;
}
}
ColumnType sqlTypeToDrift(ResolvedType? type) {
if (type == null) {
return const ColumnType.drift(DriftSqlType.string);
}
final customHint = type.hint<CustomTypeHint>();
if (customHint != null) {
return ColumnType.custom(customHint.type);
}
return ColumnType.drift(_toDefaultType(type));
}
}
/// Creates a [TypeFromText] implementation that will look up type converters
@ -176,6 +188,12 @@ class TypeConverterHint extends TypeHint {
TypeConverterHint(this.converter);
}
class CustomTypeHint extends TypeHint {
final CustomColumnType type;
CustomTypeHint(this.type);
}
class _SimpleColumn extends Column implements ColumnWithType {
@override
final String name;

View File

@ -43,31 +43,43 @@ class DriftTableResolver extends DriftElementResolver<DiscoveredDriftTable> {
for (final column in table.resultColumns) {
String? overriddenDartName;
final type = resolver.driver.typeMapping.sqlTypeToDrift(column.type);
var type = resolver.driver.typeMapping.sqlTypeToDrift(column.type);
final nullable = column.type.nullable != false;
final constraints = <DriftColumnConstraint>[];
AppliedTypeConverter? converter;
AnnotatedDartCode? defaultArgument;
String? overriddenJsonName;
final typeName = column.definition?.typeName;
final definition = column.definition;
if (definition != null) {
final typeName = definition.typeName;
final enumIndexMatch = typeName != null
? FoundReferencesInSql.enumRegex.firstMatch(typeName)
: null;
if (enumIndexMatch != null) {
final dartTypeName = enumIndexMatch.group(2)!;
final dartType = await findDartTypeOrReportError(
dartTypeName, column.definition?.typeNames?.toSingleEntity ?? stmt);
final enumIndexMatch = typeName != null
? FoundReferencesInSql.enumRegex.firstMatch(typeName)
: null;
if (dartType != null) {
converter = readEnumConverter(
(msg) => reportError(
DriftAnalysisError.inDriftFile(column.definition ?? stmt, msg)),
dartType,
type == DriftSqlType.int ? EnumType.intEnum : EnumType.textEnum,
await resolver.driver.loadKnownTypes(),
);
if (definition.typeNames case [InlineDartToken token]) {
// An inline Dart token used as a type name indicates a custom type.
final custom = await resolveCustomColumnType(token);
if (custom != null) {
type = ColumnType.custom(custom);
}
} else if (enumIndexMatch != null) {
final dartTypeName = enumIndexMatch.group(2)!;
final dartType = await findDartTypeOrReportError(dartTypeName,
column.definition?.typeNames?.toSingleEntity ?? stmt);
if (dartType != null) {
converter = readEnumConverter(
(msg) => reportError(DriftAnalysisError.inDriftFile(
column.definition ?? stmt, msg)),
dartType,
type.builtin == DriftSqlType.int
? EnumType.intEnum
: EnumType.textEnum,
await resolver.driver.loadKnownTypes(),
);
}
}
}

View File

@ -1,5 +1,4 @@
import 'package:analyzer/dart/ast/ast.dart' as dart;
import 'package:drift/drift.dart' show DriftSqlType;
import 'package:drift/drift.dart' as drift;
import 'package:recase/recase.dart';
import 'package:sqlparser/sqlparser.dart' hide ResultColumn;
@ -715,7 +714,7 @@ class QueryAnalyzer {
final type = placeholder.when(
isExpression: (e) {
final foundType = context.typeOf(e);
DriftSqlType? columnType;
ColumnType? columnType;
if (foundType.type != null) {
columnType = driver.typeMapping.sqlTypeToDrift(foundType.type);
}

View File

@ -267,10 +267,30 @@ enum EnumType {
textEnum,
}
CustomColumnType? readCustomType(
LibraryElement library,
Expression dartExpression,
KnownDriftTypes helper,
void Function(String) reportError,
) {
final staticType = dartExpression.staticType;
final asCustomType =
staticType != null ? helper.asCustomType(staticType) : null;
if (asCustomType == null) {
reportError('Not a custom type');
return null;
}
final dartType = asCustomType.typeArguments[0];
return CustomColumnType(AnnotatedDartCode.ast(dartExpression), dartType);
}
AppliedTypeConverter? readTypeConverter(
LibraryElement library,
Expression dartExpression,
DriftSqlType columnType,
ColumnType columnType,
bool columnIsNullable,
void Function(String) reportError,
KnownDriftTypes helper,
@ -369,9 +389,9 @@ AppliedTypeConverter readEnumConverter(
jsonType: columnEnumType == EnumType.intEnum
? typeProvider.intType
: typeProvider.stringType,
sqlType: columnEnumType == EnumType.intEnum
sqlType: ColumnType.drift(columnEnumType == EnumType.intEnum
? DriftSqlType.int
: DriftSqlType.string,
: DriftSqlType.string),
dartTypeIsNullable: false,
sqlTypeIsNullable: false,
isDriftEnumTypeConverter: true,
@ -415,7 +435,7 @@ void _checkParameterType(
}
bool checkType(
DriftSqlType columnType,
ColumnType columnType,
bool columnIsNullable,
AppliedTypeConverter? typeConverter,
DartType typeToCheck,
@ -467,8 +487,12 @@ DartType regularColumnType(
}
extension on TypeProvider {
DartType typeFor(DriftSqlType type, KnownDriftTypes knownTypes) {
switch (type) {
DartType typeFor(ColumnType type, KnownDriftTypes knownTypes) {
if (type.custom case CustomColumnType custom) {
return custom.dartType;
}
switch (type.builtin) {
case DriftSqlType.int:
return intType;
case DriftSqlType.bigInt:

View File

@ -1,5 +1,4 @@
import 'package:analyzer/dart/element/type.dart';
import 'package:drift/drift.dart' show DriftSqlType;
import 'package:json_annotation/json_annotation.dart';
import 'package:sqlparser/sqlparser.dart' show GeneratedAs, ReferenceAction;
import 'package:sqlparser/utils/node_to_text.dart';
@ -15,7 +14,7 @@ part '../../generated/analysis/results/column.g.dart';
class DriftColumn implements HasType {
@override
final DriftSqlType sqlType;
final ColumnType sqlType;
@override
bool get isArray => false;
@ -127,6 +126,15 @@ class DriftColumn implements HasType {
}
}
class CustomColumnType {
/// The Dart expression creating an instance of the `CustomSqlType` responsible
/// for the column.
final AnnotatedDartCode expression;
final DartType dartType;
CustomColumnType(this.expression, this.dartType);
}
class AppliedTypeConverter {
/// The Dart expression creating an instance of the applied type converter.
final AnnotatedDartCode expression;
@ -136,7 +144,7 @@ class AppliedTypeConverter {
/// The JSON type representation of this column, if this type converter
/// applies to the JSON serialization as well.
final DartType? jsonType;
final DriftSqlType sqlType;
final ColumnType sqlType;
late DriftColumn? owningColumn;

View File

@ -128,13 +128,18 @@ class AnnotatedDartCodeBuilder {
void addDriftType(HasType hasType) {
void addNonListType() {
final converter = hasType.typeConverter;
final customType = hasType.sqlType.custom;
if (converter != null) {
final nullable = converter.canBeSkippedForNulls && hasType.nullable;
addDartType(converter.dartType);
if (nullable) addText('?');
} else if (customType != null) {
addDartType(customType.dartType);
if (hasType.nullable) addText('?');
} else {
addTopLevel(dartTypeNames[hasType.sqlType]!);
addTopLevel(dartTypeNames[hasType.sqlType.builtin]!);
if (hasType.nullable) addText('?');
}
}

View File

@ -722,7 +722,7 @@ final class ScalarResultColumn extends ResultColumn
implements HasType, ArgumentForQueryRowType {
final String name;
@override
final DriftSqlType sqlType;
final ColumnType sqlType;
@override
final bool nullable;
@ -746,17 +746,22 @@ final class ScalarResultColumn extends ResultColumn
return dartNameForSqlColumn(name, existingNames: existingNames);
}
int get _columnTypeCompatibilityHash {
return Object.hash(sqlType.builtin, sqlType.custom?.dartType);
}
@override
int get compatibilityHashCode {
return Object.hash(
ScalarResultColumn, name, sqlType, nullable, typeConverter);
return Object.hash(ScalarResultColumn, name, _columnTypeCompatibilityHash,
nullable, typeConverter);
}
@override
bool isCompatibleTo(ResultColumn other) {
return other is ScalarResultColumn &&
other.name == name &&
other.sqlType == sqlType &&
other.sqlType.builtin == sqlType.builtin &&
other.sqlType.custom?.dartType == sqlType.custom?.dartType &&
other.nullable == nullable &&
other.typeConverter == typeConverter;
}
@ -924,7 +929,7 @@ class FoundVariable extends FoundElement implements HasType {
/// The (inferred) type for this variable.
@override
final DriftSqlType sqlType;
final ColumnType sqlType;
/// The type converter to apply before writing this value.
@override
@ -1014,7 +1019,7 @@ class SimpleDartPlaceholderType extends DartPlaceholderType {
class ExpressionDartPlaceholderType extends DartPlaceholderType {
/// The sql type of this expression.
final DriftSqlType? columnType;
final ColumnType? columnType;
final Expression? defaultValue;
ExpressionDartPlaceholderType(this.columnType, this.defaultValue);

View File

@ -7,6 +7,7 @@ import 'element.dart';
import 'column.dart';
import 'result_sets.dart';
import 'types.dart';
class DriftTable extends DriftElementWithResultSet {
@override
@ -82,7 +83,7 @@ class DriftTable extends DriftElementWithResultSet {
this.attachedIndices = const [],
}) {
_rowIdColumn = DriftColumn(
sqlType: DriftSqlType.int,
sqlType: ColumnType.drift(DriftSqlType.int),
nullable: false,
nameInSql: 'rowid',
nameInDart: 'rowid',
@ -126,8 +127,9 @@ class DriftTable extends DriftElementWithResultSet {
final primaryKey = fullPrimaryKey;
if (primaryKey.length == 1) {
final column = primaryKey.single;
if (column.sqlType == DriftSqlType.int ||
column.sqlType == DriftSqlType.bigInt) {
final builtinType = column.sqlType.builtin;
if (builtinType == DriftSqlType.int ||
builtinType == DriftSqlType.bigInt) {
// So this column is an alias for the rowid
return column;
}

View File

@ -17,16 +17,47 @@ abstract class HasType {
bool get isArray;
/// The associated sql type.
DriftSqlType get sqlType;
ColumnType get sqlType;
/// The applied type converter, or null if no type converter has been applied
/// to this column.
AppliedTypeConverter? get typeConverter;
}
/// The underlying SQL type of a column analyzed by drift.
///
/// We distinguish between types directly supported by drift, and types that
/// are supplied by another library. Custom types can hold different Dart types,
/// but are a feature distinct from type converters: They indicate that a type
/// is directly supported by the underlying database driver, whereas a type
/// converter is a mapping done in drift.
///
/// In addition to the SQL type, we also track whether a column is nullable,
/// appears where an array is expected or has a type converter applied to it.
/// [HasType] is the interface for sql-typed elements and is implemented by
/// columns.
class ColumnType {
/// The builtin drift type used by this column.
///
/// Even though it's unused there, custom types also have this field set -
/// to [DriftSqlType.any] because drift doesn't reinterpret these values at
/// all.
final DriftSqlType builtin;
/// Details about the custom type, if one is present.
final CustomColumnType? custom;
bool get isCustom => custom != null;
const ColumnType.drift(this.builtin) : custom = null;
ColumnType.custom(CustomColumnType this.custom) : builtin = DriftSqlType.any;
}
extension OperationOnTypes on HasType {
bool get isUint8ListInDart =>
sqlType == DriftSqlType.blob && typeConverter == null;
bool get isUint8ListInDart {
return sqlType.builtin == DriftSqlType.blob && typeConverter == null;
}
/// Whether this type is nullable in Dart
bool get nullableInDart {
@ -52,15 +83,3 @@ Map<DriftSqlType, DartTopLevelSymbol> dartTypeNames = Map.unmodifiable({
DriftSqlType.double: DartTopLevelSymbol('double', Uri.parse('dart:core')),
DriftSqlType.any: DartTopLevelSymbol('DriftAny', AnnotatedDartCode.drift),
});
/// Maps from a column type to code that can be used to create a variable of the
/// respective type.
const Map<DriftSqlType, String> createVariable = {
DriftSqlType.bool: 'Variable.withBool',
DriftSqlType.string: 'Variable.withString',
DriftSqlType.int: 'Variable.withInt',
DriftSqlType.bigInt: 'Variable.withBigInt',
DriftSqlType.dateTime: 'Variable.withDateTime',
DriftSqlType.blob: 'Variable.withBlob',
DriftSqlType.double: 'Variable.withReal',
};

View File

@ -194,9 +194,23 @@ class ElementSerializer {
};
}
Map<String, Object?> _serializeColumnType(ColumnType type) {
final custom = type.custom;
return {
if (custom != null)
'custom': {
'dart': _serializeType(custom.dartType),
'expression': custom.expression.toJson(),
}
else
'builtin': type.builtin.name,
};
}
Map<String, Object?> _serializeColumn(DriftColumn column) {
return {
'sqlType': column.sqlType.name,
'sqlType': _serializeColumnType(column.sqlType),
'nullable': column.nullable,
'nameInSql': column.nameInSql,
'nameInDart': column.nameInDart,
@ -306,7 +320,7 @@ class ElementSerializer {
'expression': converter.expression.toJson(),
'dart_type': _serializeType(converter.dartType),
'json_type': _serializeType(converter.jsonType),
'sql_type': converter.sqlType.name,
'sql_type': _serializeColumnType(converter.sqlType),
'dart_type_is_nullable': converter.dartTypeIsNullable,
'sql_type_is_nullable': converter.sqlTypeIsNullable,
'is_drift_enum_converter': converter.isDriftEnumTypeConverter,
@ -709,11 +723,24 @@ class ElementDeserializer {
}
}
Future<ColumnType> _readColumnType(Map json, Uri definition) async {
if (json.containsKey('custom')) {
return ColumnType.custom(CustomColumnType(
AnnotatedDartCode.fromJson(json['expression'] as Map),
await _readDartType(definition, json['dart'] as int),
));
} else {
return ColumnType.drift(
DriftSqlType.values.byName(json['builtin'] as String));
}
}
Future<DriftColumn> _readColumn(Map json, DriftElementId ownTable) async {
final rawConverter = json['typeConverter'] as Map?;
return DriftColumn(
sqlType: DriftSqlType.values.byName(json['sqlType'] as String),
sqlType:
await _readColumnType(json['sqlType'] as Map, ownTable.libraryUri),
nullable: json['nullable'] as bool,
nameInSql: json['nameInSql'] as String,
nameInDart: json['nameInDart'] as String,
@ -752,7 +779,7 @@ class ElementDeserializer {
jsonType: json['json_type'] != null
? await _readDartType(definition, json['json_type'] as int)
: null,
sqlType: DriftSqlType.values.byName(json['sql_type'] as String),
sqlType: await _readColumnType(json['sql_type'] as Map, definition),
dartTypeIsNullable: json['dart_type_is_nullable'] as bool,
sqlTypeIsNullable: json['sql_type_is_nullable'] as bool,
isDriftEnumTypeConverter: json['is_drift_enum_converter'] as bool,

View File

@ -158,7 +158,7 @@ class SchemaWriter {
return {
'name': column.nameInSql,
'getter_name': column.nameInDart,
'moor_type': column.sqlType.toSerializedString(),
'moor_type': column.sqlType.builtin.toSerializedString(),
'nullable': column.nullable,
'customConstraints': column.customConstraints,
if (constraints[SqlDialect.sqlite]!.isNotEmpty &&
@ -467,7 +467,7 @@ class SchemaReader {
// Note: Not including client default code because that usually depends on
// imports from the database.
return DriftColumn(
sqlType: columnType,
sqlType: ColumnType.drift(columnType),
nullable: nullable,
nameInSql: name,
nameInDart: getterName ?? ReCase(name).camelCase,

View File

@ -59,7 +59,7 @@ class FunctionStubsWriter {
String _nameFor(String sqlName) => ReCase(sqlName).camelCase;
void _writeTypeFor(ResolvedType type) {
final driftType = _driver.typeMapping.sqlTypeToDrift(type);
final driftType = _driver.typeMapping.sqlTypeToDrift(type).builtin;
_emitter.writeDart(AnnotatedDartCode([dartTypeNames[driftType]!]));
if (type.nullable == true) {

View File

@ -202,10 +202,19 @@ class QueryWriter {
}
final dartLiteral = asDartLiteral(name);
final method = isNullable ? 'readNullable' : 'read';
final rawDartType =
_emitter.dartCode(AnnotatedDartCode([dartTypeNames[column.sqlType]!]));
var code = 'row.$method<$rawDartType>($dartLiteral)';
_emitter.dartCode(_emitter.innerColumnType(column.sqlType));
String code;
if (column.sqlType.isCustom) {
final method = isNullable ? 'readNullableWithType' : 'readWithType';
final typeImpl = _emitter.dartCode(column.sqlType.custom!.expression);
code = 'row.$method<$rawDartType>($dartLiteral, $typeImpl)';
} else {
final method = isNullable ? 'readNullable' : 'read';
code = 'row.$method<$rawDartType>($dartLiteral)';
}
final converter = column.typeConverter;
if (converter != null) {
@ -838,8 +847,8 @@ class _ExpandedVariableWriter {
// write all the variables sequentially.
String constructVar(String dartExpr) {
// No longer an array here, we apply a for loop if necessary
final type =
_emitter.dartCode(_emitter.innerColumnType(element, nullable: false));
final type = _emitter
.dartCode(_emitter.innerColumnType(element.sqlType, nullable: false));
final varType = _emitter.drift('Variable');
final buffer = StringBuffer('$varType<$type>(');

View File

@ -26,7 +26,7 @@ extension FoundElementType on FoundElement {
builder
..addSymbol('Expression', AnnotatedDartCode.drift)
..addText('<')
..addTopLevel(dartTypeNames[kind.columnType]!)
..addCode(scope.innerColumnType(kind.columnType!))
..addText('>');
} else if (kind is InsertableDartPlaceholderType) {
final table = kind.table;

View File

@ -47,7 +47,7 @@ final class _TableShape {
DriftElementWithResultSet e) {
return {
for (final column in e.columns)
column.nameInDart: (column.nameInSql, column.sqlType),
column.nameInDart: (column.nameInSql, column.sqlType.builtin),
};
}
}

View File

@ -339,7 +339,13 @@ class RowMappingWriter {
final columnName = column.nameInSql;
final rawData = "data['\${effectivePrefix}$columnName']";
final sqlType = writer.drift(column.sqlType.toString());
String sqlType;
if (column.sqlType.custom case CustomColumnType custom) {
sqlType = writer.dartCode(custom.expression);
} else {
sqlType = writer.drift(column.sqlType.builtin.toString());
}
var loadType = '$databaseGetter.typeMapping.read($sqlType, $rawData)';
if (!column.nullable) {

View File

@ -210,7 +210,13 @@ abstract class TableOrViewWriter {
}
}
additionalParams['type'] = emitter.drift(column.sqlType.toString());
if (column.sqlType.isCustom) {
additionalParams['type'] =
emitter.dartCode(column.sqlType.custom!.expression);
} else {
additionalParams['type'] =
emitter.drift(column.sqlType.builtin.toString());
}
if (isRequiredForInsert != null) {
additionalParams['requiredDuringInsert'] = isRequiredForInsert.toString();
@ -256,7 +262,7 @@ abstract class TableOrViewWriter {
emitter.dartCode(column.clientDefaultCode!);
}
final innerType = emitter.innerColumnType(column);
final innerType = emitter.innerColumnType(column.sqlType);
var type =
'${emitter.drift('GeneratedColumn')}<${emitter.dartCode(innerType)}>';
expressionBuffer

View File

@ -135,7 +135,8 @@ class UpdateCompanionWriter {
final expression = _emitter.drift('Expression');
for (final column in columns) {
final typeName = _emitter.dartCode(_emitter.innerColumnType(column));
final typeName =
_emitter.dartCode(_emitter.innerColumnType(column.sqlType));
_buffer.write('$expression<$typeName>? ${column.nameInDart}, \n');
}
@ -190,32 +191,35 @@ class UpdateCompanionWriter {
for (final column in columns) {
final getterName = thisIfNeeded(column.nameInDart, locals);
_buffer.write('if ($getterName.present) {');
_buffer.writeln('if ($getterName.present) {');
final typeName =
_emitter.dartCode(_emitter.variableTypeCode(column, nullable: false));
final mapSetter = 'map[${asDartLiteral(column.nameInSql)}] = '
'${_emitter.drift('Variable')}<$typeName>';
var value = '$getterName.value';
final converter = column.typeConverter;
if (converter != null) {
// apply type converter before writing the variable
final fieldName = _emitter.dartCode(
_emitter.readConverter(converter, forNullable: column.nullable));
_buffer
..write('final converter = $fieldName;\n')
..write(mapSetter)
..write('(converter.toSql($getterName.value)')
..write(');');
} else {
// no type converter. Write variable directly
_buffer
..write(mapSetter)
..write('(')
..write('$getterName.value')
..write(');');
_buffer.writeln('final converter = $fieldName;\n');
value = 'converter.toSql($value)';
}
_buffer.write('}');
_buffer
..write(mapSetter)
..write('($value');
if (column.sqlType.isCustom) {
// Also specify the custom type since it can't be inferred from the
// value passed to the variable.
_buffer
..write(', ')
..write(_emitter.dartCode(column.sqlType.custom!.expression));
}
_buffer.writeln(');}');
}
_buffer.write('return map; \n}\n');

View File

@ -72,7 +72,7 @@ Map<SqlDialect, String> defaultConstraints(DriftColumn column) {
}
}
if (column.sqlType == DriftSqlType.bool) {
if (column.sqlType.builtin == DriftSqlType.bool) {
final name = column.nameInSql;
dialectSpecificConstraints[SqlDialect.sqlite]!
.add('CHECK (${SqlDialect.sqlite.escape(name)} IN (0, 1))');

View File

@ -134,7 +134,16 @@ abstract class _NodeOrWriter {
{bool makeNullable = false}) {
// Write something like `TypeConverter<MyFancyObject, String>`
return AnnotatedDartCode.build((b) {
var sqlDartType = dartTypeNames[converter.sqlType]!;
AnnotatedDartCode sqlDartType;
if (converter.sqlType.isCustom) {
sqlDartType =
AnnotatedDartCode.type(converter.sqlType.custom!.dartType);
} else {
sqlDartType =
AnnotatedDartCode([dartTypeNames[converter.sqlType.builtin]!]);
}
final className = converter.alsoAppliesToJsonConversion
? 'JsonTypeConverter2'
: 'TypeConverter';
@ -145,7 +154,7 @@ abstract class _NodeOrWriter {
..addDartType(converter.dartType)
..questionMarkIfNullable(makeNullable)
..addText(',')
..addTopLevel(sqlDartType)
..addCode(sqlDartType)
..questionMarkIfNullable(makeNullable || converter.sqlTypeIsNullable);
if (converter.alsoAppliesToJsonConversion) {
@ -169,7 +178,8 @@ abstract class _NodeOrWriter {
/// This is the same as [dartType] but without custom types.
AnnotatedDartCode variableTypeCode(HasType type, {bool? nullable}) {
if (type.isArray) {
final inner = innerColumnType(type, nullable: nullable ?? type.nullable);
final inner =
innerColumnType(type.sqlType, nullable: nullable ?? type.nullable);
return AnnotatedDartCode([
DartTopLevelSymbol.list,
'<',
@ -177,7 +187,7 @@ abstract class _NodeOrWriter {
'>',
]);
} else {
return innerColumnType(type, nullable: nullable ?? type.nullable);
return innerColumnType(type.sqlType, nullable: nullable ?? type.nullable);
}
}
@ -185,11 +195,20 @@ abstract class _NodeOrWriter {
/// [nullable] parameter.
///
/// This type does not respect type converters or arrays.
AnnotatedDartCode innerColumnType(HasType type, {bool nullable = false}) {
return AnnotatedDartCode([
dartTypeNames[type.sqlType],
if (nullable) '?',
]);
AnnotatedDartCode innerColumnType(ColumnType type, {bool nullable = false}) {
return AnnotatedDartCode.build((b) {
final custom = type.custom;
if (custom != null) {
b.addDartType(custom.dartType);
} else {
b.addTopLevel(dartTypeNames[type.builtin]!);
}
if (nullable) {
b.addText('?');
}
});
}
String refUri(Uri definition, String element) {

View File

@ -172,7 +172,8 @@ class ProgrammingLanguages extends Table {
final tablesFile = await backend.analyze('package:a/tables.drift');
final librariesQuery = tablesFile.fileAnalysis!.resolvedQueries.values
.singleWhere((e) => e.name == 'findLibraries') as SqlSelectQuery;
expect(librariesQuery.variables.single.sqlType, DriftSqlType.string);
expect(
librariesQuery.variables.single.sqlType.builtin, DriftSqlType.string);
expect(librariesQuery.declaredInDriftFile, isTrue);
});

View File

@ -1,3 +1,4 @@
import 'package:drift/drift.dart';
import 'package:drift_dev/src/analysis/options.dart';
import 'package:drift_dev/src/analysis/results/results.dart';
import 'package:test/test.dart';
@ -233,6 +234,30 @@ class Database {}
expect(column.nameInSql, 'TEXTCOLUMN');
});
test('recognizes custom column types', () async {
final state = TestBackend.inTest({
'a|lib/main.dart': '''
import 'package:drift/drift.dart';
class StringArrayType implements CustomSqlType<List<String>> {}
class TestTable extends Table {
Column<List<String>> get list => customType(StringArrayType())();
}
''',
});
final file = await state.analyze('package:a/main.dart');
state.expectNoErrors();
final table = file.analyzedElements.whereType<DriftTable>().single;
final column = table.columns.single;
expect(column.sqlType.builtin, DriftSqlType.any);
expect(column.sqlType.custom?.dartType.toString(), 'List<String>');
expect(column.sqlType.custom?.expression.toString(), 'StringArrayType()');
});
group('customConstraint analysis', () {
test('reports errors', () async {
final state = TestBackend.inTest({

View File

@ -95,7 +95,7 @@ abstract class TodoItemWithCategoryNameView extends View {
todoCategoryItemCount.columns[1],
isA<DriftColumn>()
.having((e) => e.nameInDart, 'nameInDart', 'itemCount')
.having((e) => e.sqlType, 'sqlType', DriftSqlType.int)
.having((e) => e.sqlType.builtin, 'sqlType', DriftSqlType.int)
.having((e) => e.nullable, 'nullable', isTrue));
expect(todoItemWithCategoryName.columns, hasLength(2));
@ -108,7 +108,7 @@ abstract class TodoItemWithCategoryNameView extends View {
todoItemWithCategoryName.columns[1],
isA<DriftColumn>()
.having((e) => e.nameInDart, 'nameInDart', 'title')
.having((e) => e.sqlType, 'sqlType', DriftSqlType.string)
.having((e) => e.sqlType.builtin, 'sqlType', DriftSqlType.string)
.having((e) => e.nullable, 'nullable', isTrue));
});
}

View File

@ -23,8 +23,8 @@ void main() {
final view = file.analyzedElements.single as DriftView;
expect(view.columns, [
isA<DriftColumn>()
.having((e) => e.sqlType, 'sqlType', drift.DriftSqlType.string)
isA<DriftColumn>().having(
(e) => e.sqlType.builtin, 'sqlType', drift.DriftSqlType.string)
]);
expect(view.references,
@ -56,8 +56,8 @@ void main() {
expect(parentView.columns, hasLength(2));
expect(childView.columns, [
isA<DriftColumn>()
.having((e) => e.sqlType, 'sqlType', drift.DriftSqlType.string)
isA<DriftColumn>().having(
(e) => e.sqlType.builtin, 'sqlType', drift.DriftSqlType.string)
]);
expect(parentView.references.map((e) => e.id.name), ['t']);
@ -290,7 +290,7 @@ enum MyEnum {
final view = state.analyzedElements.single as DriftView;
final column = view.columns.single;
expect(column.sqlType, expectedType);
expect(column.sqlType.builtin, expectedType);
expect(
view.source,
isA<SqlViewSource>().having(

View File

@ -33,7 +33,8 @@ SELECT
final resultSet = query.resultSet;
expect(resultSet.singleColumn, isTrue);
expect(resultSet.needsOwnClass, isFalse);
expect(resultSet.scalarColumns.map((c) => c.sqlType), [DriftSqlType.int]);
expect(resultSet.scalarColumns.map((c) => c.sqlType.builtin),
[DriftSqlType.int]);
});
test('recognizes CTE clause', () async {
@ -64,7 +65,8 @@ WITH RECURSIVE
expect(resultSet.singleColumn, isTrue);
expect(resultSet.needsOwnClass, isFalse);
expect(resultSet.columns.map(resultSet.dartNameFor), ['x']);
expect(resultSet.scalarColumns.map((c) => c.sqlType), [DriftSqlType.int]);
expect(resultSet.scalarColumns.map((c) => c.sqlType.builtin),
[DriftSqlType.int]);
});
test('finds the underlying table when aliased through CTE', () async {

View File

@ -108,7 +108,7 @@ wrongArgs: SELECT sin(oid, foo) FROM numbers;
expect(
queryInA.resultSet.scalarColumns.single,
const TypeMatcher<ScalarResultColumn>()
.having((e) => e.sqlType, 'type', DriftSqlType.double),
.having((e) => e.sqlType.builtin, 'type', DriftSqlType.double),
);
final fileB = await state.analyze('package:foo/b.drift');

View File

@ -33,12 +33,12 @@ CREATE TABLE b (
final b = results[1].result! as DriftTable;
final bBar = b.columns[0];
expect(aFoo.sqlType, DriftSqlType.int);
expect(aFoo.sqlType.builtin, DriftSqlType.int);
expect(aFoo.nullable, isFalse);
expect(aFoo.constraints, [isA<PrimaryKeyColumn>()]);
expect(aFoo.customConstraints, 'PRIMARY KEY');
expect(aBar.sqlType, DriftSqlType.int);
expect(aBar.sqlType.builtin, DriftSqlType.int);
expect(aBar.nullable, isTrue);
expect(aBar.constraints, [
isA<ForeignKeyReference>()
@ -48,7 +48,7 @@ CREATE TABLE b (
]);
expect(aBar.customConstraints, 'REFERENCES b(bar)');
expect(bBar.sqlType, DriftSqlType.int);
expect(bBar.sqlType.builtin, DriftSqlType.int);
expect(bBar.nullable, isFalse);
expect(bBar.constraints, isEmpty);
expect(bBar.customConstraints, 'NOT NULL');
@ -111,7 +111,7 @@ CREATE TABLE b (
final indexColumn =
table.columns.singleWhere((c) => c.nameInSql == 'fruitIndex');
expect(indexColumn.sqlType, DriftSqlType.int);
expect(indexColumn.sqlType.builtin, DriftSqlType.int);
expect(
indexColumn.typeConverter,
isA<AppliedTypeConverter>()
@ -126,7 +126,7 @@ CREATE TABLE b (
final withGenericIndexColumn = table.columns
.singleWhere((c) => c.nameInSql == 'fruitWithGenericIndex');
expect(withGenericIndexColumn.sqlType, DriftSqlType.int);
expect(withGenericIndexColumn.sqlType.builtin, DriftSqlType.int);
expect(
withGenericIndexColumn.typeConverter,
isA<AppliedTypeConverter>()
@ -142,7 +142,7 @@ CREATE TABLE b (
final nameColumn =
table.columns.singleWhere((c) => c.nameInSql == 'fruitName');
expect(nameColumn.sqlType, DriftSqlType.string);
expect(nameColumn.sqlType.builtin, DriftSqlType.string);
expect(
nameColumn.typeConverter,
isA<AppliedTypeConverter>()
@ -263,4 +263,31 @@ CREATE TABLE IF NOT EXISTS currencies (
'documentationComment', '/// The name of this currency'),
);
});
test('can use custom types', () async {
final state = TestBackend.inTest({
'a|lib/a.drift': '''
import 'b.dart';
CREATE TABLE foo (
bar `MyType()` NOT NULL
);
''',
'a|lib/b.dart': '''
import 'package:drift/drift.dart';
class MyType implements CustomSqlType<String> {}
''',
});
final file = await state.analyze('package:a/a.drift');
state.expectNoErrors();
final table = file.analyzedElements.single as DriftTable;
final column = table.columns.single;
expect(column.sqlType.isCustom, isTrue);
expect(column.sqlType.custom?.dartType.toString(), 'String');
expect(column.sqlType.custom?.expression.toString(), 'MyType()');
});
}

View File

@ -24,7 +24,7 @@ bar(?1 AS TEXT, :foo AS BOOLEAN): SELECT ?, :foo;
final resultSet = (query as SqlSelectQuery).resultSet;
expect(resultSet.matchingTable, isNull);
expect(resultSet.scalarColumns.map((c) => c.name), ['?', ':foo']);
expect(resultSet.scalarColumns.map((c) => c.sqlType),
expect(resultSet.scalarColumns.map((c) => c.sqlType.builtin),
[DriftSqlType.string, DriftSqlType.bool]);
});
@ -181,19 +181,22 @@ q3: SELECT datetime('now');
expect(queries, hasLength(3));
final q1 = queries[0];
expect(q1.resultSet!.scalarColumns.single.sqlType, DriftSqlType.dateTime);
expect(q1.resultSet!.scalarColumns.single.sqlType.builtin,
DriftSqlType.dateTime);
final q2 = queries[1];
final q3 = queries[2];
if (dateTimeAsText) {
expect(q2.resultSet!.scalarColumns.single.sqlType, DriftSqlType.int);
expect(
q3.resultSet!.scalarColumns.single.sqlType, DriftSqlType.dateTime);
expect(q2.resultSet!.scalarColumns.single.sqlType.builtin,
DriftSqlType.int);
expect(q3.resultSet!.scalarColumns.single.sqlType.builtin,
DriftSqlType.dateTime);
} else {
expect(
q2.resultSet!.scalarColumns.single.sqlType, DriftSqlType.dateTime);
expect(q3.resultSet!.scalarColumns.single.sqlType, DriftSqlType.string);
expect(q2.resultSet!.scalarColumns.single.sqlType.builtin,
DriftSqlType.dateTime);
expect(q3.resultSet!.scalarColumns.single.sqlType.builtin,
DriftSqlType.string);
}
});
}
@ -306,11 +309,12 @@ LEFT JOIN tableB1 AS tableB2 -- nullable
final query = result.fileAnalysis!.resolvedQueries.values.single;
expect(query.resultSet!.columns, [
isA<ScalarResultColumn>()
.having((e) => e.sqlType, 'sqlType', DriftSqlType.bool)
.having((e) => e.sqlType.builtin, 'sqlType', DriftSqlType.bool)
]);
final args = query.variables;
expect(args.map((e) => e.sqlType), [DriftSqlType.int, DriftSqlType.string]);
expect(args.map((e) => e.sqlType.builtin),
[DriftSqlType.int, DriftSqlType.string]);
});
test('can cast to DATETIME and BOOLEAN', () async {
@ -325,9 +329,10 @@ a: SELECT CAST(1 AS BOOLEAN) AS a, CAST(2 AS DATETIME) as b;
final resultSet = query.resultSet!;
expect(resultSet.columns, [
scalarColumn('a').having((e) => e.sqlType, 'sqlType', DriftSqlType.bool),
scalarColumn('a')
.having((e) => e.sqlType.builtin, 'sqlType', DriftSqlType.bool),
scalarColumn('b')
.having((e) => e.sqlType, 'sqlType', DriftSqlType.dateTime),
.having((e) => e.sqlType.builtin, 'sqlType', DriftSqlType.dateTime),
]);
});

View File

@ -279,7 +279,8 @@ class _HasInferredColumnTypes extends CustomMatcher {
final resultSet = actual.resultSet;
return {
for (final column in resultSet.scalarColumns) column.name: column.sqlType
for (final column in resultSet.scalarColumns)
column.name: column.sqlType.builtin
};
}
}

View File

@ -15,7 +15,7 @@ void main() {
DriftDeclaration(fakeUri, -1, ''),
columns: [
DriftColumn(
sqlType: DriftSqlType.int,
sqlType: ColumnType.drift(DriftSqlType.int),
nullable: false,
nameInSql: 'foo',
nameInDart: 'foo',

View File

@ -0,0 +1,37 @@
import 'package:drift/drift.dart';
import 'package:drift_postgres/postgres.dart';
import 'package:postgres/postgres_v3_experimental.dart';
import 'package:uuid/uuid.dart';
part 'main.g.dart';
class Users extends Table {
UuidColumn get id => customType(PgTypes.uuid).withDefault(genRandomUuid())();
TextColumn get name => text()();
}
@DriftDatabase(tables: [Users])
class DriftPostgresDatabase extends _$DriftPostgresDatabase {
DriftPostgresDatabase(super.e);
@override
int get schemaVersion => 1;
}
void main() async {
final database = DriftPostgresDatabase(PgDatabase(
endpoint: PgEndpoint(
host: 'localhost',
database: 'postgres',
username: 'postgres',
password: 'postgres',
),
logStatements: true,
));
final user = await database.users.insertReturning(
UsersCompanion.insert(name: 'Simon', id: Value(Uuid().v4obj())));
print(user);
await database.close();
}

View File

@ -0,0 +1,192 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'main.dart';
// ignore_for_file: type=lint
class $UsersTable extends Users with TableInfo<$UsersTable, User> {
@override
final GeneratedDatabase attachedDatabase;
final String? _alias;
$UsersTable(this.attachedDatabase, [this._alias]);
static const VerificationMeta _idMeta = const VerificationMeta('id');
@override
late final GeneratedColumn<UuidValue> id = GeneratedColumn<UuidValue>(
'id', aliasedName, false,
type: PgTypes.uuid,
requiredDuringInsert: false,
defaultValue: genRandomUuid());
static const VerificationMeta _nameMeta = const VerificationMeta('name');
@override
late final GeneratedColumn<String> name = GeneratedColumn<String>(
'name', aliasedName, false,
type: DriftSqlType.string, requiredDuringInsert: true);
@override
List<GeneratedColumn> get $columns => [id, name];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'users';
@override
VerificationContext validateIntegrity(Insertable<User> instance,
{bool isInserting = false}) {
final context = VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('id')) {
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
}
if (data.containsKey('name')) {
context.handle(
_nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta));
} else if (isInserting) {
context.missing(_nameMeta);
}
return context;
}
@override
Set<GeneratedColumn> get $primaryKey => const {};
@override
User map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return User(
id: attachedDatabase.typeMapping
.read(PgTypes.uuid, data['${effectivePrefix}id'])!,
name: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}name'])!,
);
}
@override
$UsersTable createAlias(String alias) {
return $UsersTable(attachedDatabase, alias);
}
}
class User extends DataClass implements Insertable<User> {
final UuidValue id;
final String name;
const User({required this.id, required this.name});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
map['id'] = Variable<UuidValue>(id);
map['name'] = Variable<String>(name);
return map;
}
UsersCompanion toCompanion(bool nullToAbsent) {
return UsersCompanion(
id: Value(id),
name: Value(name),
);
}
factory User.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return User(
id: serializer.fromJson<UuidValue>(json['id']),
name: serializer.fromJson<String>(json['name']),
);
}
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<UuidValue>(id),
'name': serializer.toJson<String>(name),
};
}
User copyWith({UuidValue? id, String? name}) => User(
id: id ?? this.id,
name: name ?? this.name,
);
@override
String toString() {
return (StringBuffer('User(')
..write('id: $id, ')
..write('name: $name')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(id, name);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is User && other.id == this.id && other.name == this.name);
}
class UsersCompanion extends UpdateCompanion<User> {
final Value<UuidValue> id;
final Value<String> name;
final Value<int> rowid;
const UsersCompanion({
this.id = const Value.absent(),
this.name = const Value.absent(),
this.rowid = const Value.absent(),
});
UsersCompanion.insert({
this.id = const Value.absent(),
required String name,
this.rowid = const Value.absent(),
}) : name = Value(name);
static Insertable<User> custom({
Expression<UuidValue>? id,
Expression<String>? name,
Expression<int>? rowid,
}) {
return RawValuesInsertable({
if (id != null) 'id': id,
if (name != null) 'name': name,
if (rowid != null) 'rowid': rowid,
});
}
UsersCompanion copyWith(
{Value<UuidValue>? id, Value<String>? name, Value<int>? rowid}) {
return UsersCompanion(
id: id ?? this.id,
name: name ?? this.name,
rowid: rowid ?? this.rowid,
);
}
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
if (id.present) {
map['id'] = Variable<UuidValue>(id.value, PgTypes.uuid);
}
if (name.present) {
map['name'] = Variable<String>(name.value);
}
if (rowid.present) {
map['rowid'] = Variable<int>(rowid.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('UsersCompanion(')
..write('id: $id, ')
..write('name: $name, ')
..write('rowid: $rowid')
..write(')'))
.toString();
}
}
abstract class _$DriftPostgresDatabase extends GeneratedDatabase {
_$DriftPostgresDatabase(QueryExecutor e) : super(e);
late final $UsersTable users = $UsersTable(this);
@override
Iterable<TableInfo<Table, Object?>> get allTables =>
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
@override
List<DatabaseSchemaEntity> get allSchemaEntities => [users];
}

View File

@ -1,32 +0,0 @@
import 'package:drift/backends.dart';
import 'package:drift/src/runtime/query_builder/query_builder.dart';
import 'package:drift_postgres/postgres.dart';
import 'package:postgres/postgres_v3_experimental.dart';
void main() async {
final postgres = PgDatabase(
endpoint: PgEndpoint(
host: 'localhost',
database: 'postgres',
username: 'postgres',
password: 'postgres',
),
logStatements: true,
);
await postgres.ensureOpen(_NullUser());
final rows = await postgres.runSelect(r'SELECT $1', [true]);
final row = rows.single;
print(row);
print(row.values.map((e) => e.runtimeType).toList());
}
class _NullUser extends QueryExecutorUser {
@override
Future<void> beforeOpen(
QueryExecutor executor, OpeningDetails details) async {}
@override
int get schemaVersion => 1;
}

View File

@ -2,6 +2,33 @@
@experimental
library drift.postgres;
import 'package:drift/drift.dart';
import 'package:meta/meta.dart';
import 'package:postgres/postgres_v3_experimental.dart';
import 'package:uuid/uuid.dart';
import 'src/types.dart';
export 'src/pg_database.dart';
typedef UuidColumn = Column<UuidValue>;
typedef IntervalColumn = Column<Duration>;
typedef JsonColumn = Column<Object>;
typedef PointColumn = Column<PgPoint>;
final class PgTypes {
PgTypes._();
static const CustomSqlType<UuidValue> uuid = UuidType();
static const CustomSqlType<Duration> interval = IntervalType();
static const CustomSqlType<Object> json =
PostgresType(type: PgDataType.json, name: 'json');
static const CustomSqlType<Object> jsonb =
PostgresType(type: PgDataType.json, name: 'jsonb');
static const CustomSqlType<PgPoint> point = PointType();
}
/// Calls the `gen_random_uuid` function in postgres.
Expression<UuidValue> genRandomUuid() {
return FunctionCallExpression('gen_random_uuid', []);
}

View File

@ -152,25 +152,16 @@ class _BoundArguments {
}
for (final value in args) {
if (value == null) {
add(PgTypedParameter(PgDataType.text, null));
} else if (value is int) {
add(PgTypedParameter(PgDataType.bigInteger, value));
} else if (value is BigInt) {
// Drift only uses BigInts to represent 64-bit values on the web, so we
// can use toInt() here.
add(PgTypedParameter(PgDataType.bigInteger, value));
} else if (value is bool) {
add(PgTypedParameter(PgDataType.boolean, value));
} else if (value is double) {
add(PgTypedParameter(PgDataType.double, value));
} else if (value is String) {
add(PgTypedParameter(PgDataType.text, value));
} else if (value is List<int>) {
add(PgTypedParameter(PgDataType.byteArray, value));
} else {
throw ArgumentError.value(value, 'value', 'Unsupported type');
}
add(switch (value) {
PgTypedParameter() => value,
null => PgTypedParameter(PgDataType.text, null),
int() || BigInt() => PgTypedParameter(PgDataType.bigInteger, value),
String() => PgTypedParameter(PgDataType.text, value),
bool() => PgTypedParameter(PgDataType.boolean, value),
double() => PgTypedParameter(PgDataType.double, value),
List<int>() => PgTypedParameter(PgDataType.byteArray, value),
_ => throw ArgumentError.value(value, 'value', 'Unsupported type'),
});
}
return _BoundArguments(types, parameters);

View File

@ -0,0 +1,67 @@
import 'package:drift/drift.dart';
import 'package:postgres/postgres_v3_experimental.dart';
// ignore: implementation_imports
import 'package:postgres/src/text_codec.dart';
import 'package:uuid/uuid.dart';
class PostgresType<T extends Object> implements CustomSqlType<T> {
static final _encoder = PostgresTextEncoder();
final PgDataType type;
final String name;
const PostgresType({required this.type, required this.name});
@override
String mapToSqlLiteral(T dartValue) {
return '${_encoder.convert(dartValue)}::$name';
}
@override
Object mapToSqlParameter(T dartValue) => PgTypedParameter(type, dartValue);
@override
T read(Object fromSql) => fromSql as T;
@override
String sqlTypeName(GenerationContext context) => name;
}
class UuidType extends PostgresType<UuidValue> {
const UuidType() : super(type: PgDataType.uuid, name: 'uuid');
@override
String mapToSqlLiteral(UuidValue dartValue) {
// UUIDs can't contain escape characters, so we don't check these values.
return "'${dartValue.uuid}'";
}
@override
Object mapToSqlParameter(UuidValue dartValue) {
return PgTypedParameter(PgDataType.uuid, dartValue.uuid);
}
@override
UuidValue read(Object fromSql) {
return UuidValue(fromSql as String);
}
}
// override because the text encoder doesn't properly encode PgPoint values
class PointType extends PostgresType<PgPoint> {
const PointType() : super(type: PgDataType.point, name: 'point');
@override
String mapToSqlLiteral(PgPoint dartValue) {
return "'(${dartValue.latitude}, ${dartValue.longitude})'::point";
}
}
class IntervalType extends PostgresType<Duration> {
const IntervalType() : super(type: PgDataType.interval, name: 'interval');
@override
String mapToSqlLiteral(Duration dartValue) {
return "'${dartValue.inMicroseconds} microseconds'::interval";
}
}

View File

@ -3,19 +3,22 @@ description: Postgres support for drift
version: 1.0.0
environment:
sdk: '>=2.12.0-0 <4.0.0'
sdk: '>=3.0.0 <4.0.0'
dependencies:
collection: ^1.16.0
drift: ^2.0.0
postgres:
meta: ^1.8.0
uuid: ^4.1.0
dev_dependencies:
lints: ^2.0.0
test: ^1.18.0
drift_dev:
drift_testcases:
path: ../integration_tests/drift_testcases
build_runner: ^2.4.6
dependency_overrides:
drift:

View File

@ -0,0 +1,53 @@
import 'package:drift/drift.dart';
import 'package:drift_postgres/postgres.dart';
import 'package:postgres/postgres_v3_experimental.dart';
import 'package:test/test.dart';
import 'package:uuid/uuid.dart';
import '../example/main.dart';
void main() {
final database = DriftPostgresDatabase(PgDatabase(
endpoint: PgEndpoint(
host: 'localhost',
database: 'postgres',
username: 'postgres',
password: 'postgres',
),
));
setUpAll(() async {
await database.users.insertOne(UsersCompanion.insert(name: 'test user'));
});
tearDownAll(() async {
await database.users.deleteAll();
await database.close();
});
group('custom types pass through', () {
void testWith<T extends Object>(CustomSqlType<T> type, T value) {
test('with variable', () async {
final variable = Variable(value, type);
final query = database.selectOnly(database.users)
..addColumns([variable]);
final row = await query.getSingle();
expect(row.read(variable), value);
});
test('with constant', () async {
final constant = Constant(value, type);
final query = database.selectOnly(database.users)
..addColumns([constant]);
final row = await query.getSingle();
expect(row.read(constant), value);
});
}
group('uuid', () => testWith(PgTypes.uuid, Uuid().v4obj()));
group('interval', () => testWith(PgTypes.interval, Duration(seconds: 15)));
group('json', () => testWith(PgTypes.json, {'foo': 'bar'}));
group('jsonb', () => testWith(PgTypes.jsonb, {'foo': 'bar'}));
group('point', () => testWith(PgTypes.point, PgPoint(90, -90)));
});
}

View File

@ -2496,6 +2496,10 @@ class Parser {
}
List<Token>? _typeName() {
if (enableDriftExtensions && _matchOne(TokenType.inlineDart)) {
return [_previous];
}
// sqlite doesn't really define what a type name is and has very loose rules
// at turning them into a type affinity. We support this pattern:
// typename = identifier [ "(" { identifier | comma | number_literal } ")" ]

View File

@ -217,7 +217,7 @@ void main() {
);
});
test('parses CREATE TABLE WITH in drift more', () {
test('parses CREATE TABLE WITH in drift mode', () {
testStatement(
'CREATE TABLE a (b INTEGER) WITH MyExistingClass',
CreateTableStatement(
@ -237,6 +237,23 @@ void main() {
);
});
test('parses custom types in drift mode', () {
testStatement(
'CREATE TABLE a (b `PgTypes.uuid` NOT NULL)',
CreateTableStatement(
tableName: 'a',
columns: [
ColumnDefinition(
columnName: 'b',
typeName: '`PgTypes.uuid`',
constraints: [NotNull(null)],
),
],
),
driftMode: true,
);
});
test('parses CREATE VIRTUAL TABLE statement', () {
testStatement(
'CREATE VIRTUAL TABLE IF NOT EXISTS foo USING bar(a, b(), c) AS drift',