Support custom types in postgres backend

This commit is contained in:
Simon Binder 2023-10-07 18:08:18 +02:00
parent 62bda167bd
commit 207847b02c
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
8 changed files with 305 additions and 66 deletions

View File

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

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,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<UuidValue>;
final class PgTypes {
PgTypes._();
static const CustomSqlType<UuidValue> uuid = UuidType();
}
/// 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,28 @@
import 'package:drift/drift.dart';
import 'package:postgres/postgres_v3_experimental.dart';
import 'package:uuid/uuid.dart';
class UuidType implements CustomSqlType<UuidValue> {
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';
}
}

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: