From 207847b02c1effa98721192bd07515c21ae840e9 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sat, 7 Oct 2023 18:08:18 +0200 Subject: [PATCH] Support custom types in postgres backend --- .../tables/update_companion_writer.dart | 31 +-- extras/drift_postgres/example/main.dart | 37 ++++ extras/drift_postgres/example/main.g.dart | 192 ++++++++++++++++++ extras/drift_postgres/example/test.dart | 32 --- extras/drift_postgres/lib/postgres.dart | 17 ++ .../drift_postgres/lib/src/pg_database.dart | 29 +-- extras/drift_postgres/lib/src/types.dart | 28 +++ extras/drift_postgres/pubspec.yaml | 5 +- 8 files changed, 305 insertions(+), 66 deletions(-) create mode 100644 extras/drift_postgres/example/main.dart create mode 100644 extras/drift_postgres/example/main.g.dart delete mode 100644 extras/drift_postgres/example/test.dart create mode 100644 extras/drift_postgres/lib/src/types.dart diff --git a/drift_dev/lib/src/writer/tables/update_companion_writer.dart b/drift_dev/lib/src/writer/tables/update_companion_writer.dart index b6be030f..d5abc262 100644 --- a/drift_dev/lib/src/writer/tables/update_companion_writer.dart +++ b/drift_dev/lib/src/writer/tables/update_companion_writer.dart @@ -191,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'); diff --git a/extras/drift_postgres/example/main.dart b/extras/drift_postgres/example/main.dart new file mode 100644 index 00000000..45e7d8a1 --- /dev/null +++ b/extras/drift_postgres/example/main.dart @@ -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(); +} diff --git a/extras/drift_postgres/example/main.g.dart b/extras/drift_postgres/example/main.g.dart new file mode 100644 index 00000000..3b79ed01 --- /dev/null +++ b/extras/drift_postgres/example/main.g.dart @@ -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 id = GeneratedColumn( + 'id', aliasedName, false, + type: PgTypes.uuid, + requiredDuringInsert: false, + defaultValue: genRandomUuid()); + static const VerificationMeta _nameMeta = const VerificationMeta('name'); + @override + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + @override + List get $columns => [id, name]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'users'; + @override + VerificationContext validateIntegrity(Insertable 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 get $primaryKey => const {}; + @override + User map(Map 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 { + final UuidValue id; + final String name; + const User({required this.id, required this.name}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + return map; + } + + UsersCompanion toCompanion(bool nullToAbsent) { + return UsersCompanion( + id: Value(id), + name: Value(name), + ); + } + + factory User.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return User( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(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 { + final Value id; + final Value name; + final Value 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 custom({ + Expression? id, + Expression? name, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (rowid != null) 'rowid': rowid, + }); + } + + UsersCompanion copyWith( + {Value? id, Value? name, Value? rowid}) { + return UsersCompanion( + id: id ?? this.id, + name: name ?? this.name, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value, PgTypes.uuid); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (rowid.present) { + map['rowid'] = Variable(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> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [users]; +} diff --git a/extras/drift_postgres/example/test.dart b/extras/drift_postgres/example/test.dart deleted file mode 100644 index 87c2fa30..00000000 --- a/extras/drift_postgres/example/test.dart +++ /dev/null @@ -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 beforeOpen( - QueryExecutor executor, OpeningDetails details) async {} - - @override - int get schemaVersion => 1; -} diff --git a/extras/drift_postgres/lib/postgres.dart b/extras/drift_postgres/lib/postgres.dart index ba57a666..b9ea9b68 100644 --- a/extras/drift_postgres/lib/postgres.dart +++ b/extras/drift_postgres/lib/postgres.dart @@ -2,6 +2,23 @@ @experimental library drift.postgres; +import 'package:drift/drift.dart'; import 'package:meta/meta.dart'; +import 'package:uuid/uuid.dart'; + +import 'src/types.dart'; export 'src/pg_database.dart'; + +typedef UuidColumn = Column; + +final class PgTypes { + PgTypes._(); + + static const CustomSqlType uuid = UuidType(); +} + +/// Calls the `gen_random_uuid` function in postgres. +Expression genRandomUuid() { + return FunctionCallExpression('gen_random_uuid', []); +} diff --git a/extras/drift_postgres/lib/src/pg_database.dart b/extras/drift_postgres/lib/src/pg_database.dart index 0bab0232..257ef748 100644 --- a/extras/drift_postgres/lib/src/pg_database.dart +++ b/extras/drift_postgres/lib/src/pg_database.dart @@ -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) { - 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() => PgTypedParameter(PgDataType.byteArray, value), + _ => throw ArgumentError.value(value, 'value', 'Unsupported type'), + }); } return _BoundArguments(types, parameters); diff --git a/extras/drift_postgres/lib/src/types.dart b/extras/drift_postgres/lib/src/types.dart new file mode 100644 index 00000000..399591a6 --- /dev/null +++ b/extras/drift_postgres/lib/src/types.dart @@ -0,0 +1,28 @@ +import 'package:drift/drift.dart'; +import 'package:postgres/postgres_v3_experimental.dart'; +import 'package:uuid/uuid.dart'; + +class UuidType implements CustomSqlType { + const UuidType(); + + @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 + String sqlTypeName(GenerationContext context) { + return 'uuid'; + } +} diff --git a/extras/drift_postgres/pubspec.yaml b/extras/drift_postgres/pubspec.yaml index 1bf14d5d..1368d6f7 100644 --- a/extras/drift_postgres/pubspec.yaml +++ b/extras/drift_postgres/pubspec.yaml @@ -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: