Updated ORM insert for MySQL

This commit is contained in:
thomashii 2022-02-03 17:47:36 +08:00
parent bab4e5eaf6
commit cd31c040ba
31 changed files with 226 additions and 98 deletions

View file

@ -35,6 +35,6 @@ dev_dependencies:
io: ^1.0.0 io: ^1.0.0
test: ^1.17.5 test: ^1.17.5
lints: ^1.0.0 lints: ^1.0.0
dependency_overrides: #dependency_overrides:
angel3_container: # angel3_container:
path: ../container/angel_container # path: ../container/angel_container

View file

@ -26,8 +26,9 @@ class _FakeExecutor extends QueryExecutor {
@override @override
Future<List<List>> query( Future<List<List>> query(
String tableName, String? query, Map<String, dynamic> substitutionValues, String tableName, String query, Map<String, dynamic> substitutionValues,
[returningFields = const []]) async { {String returningQuery = '',
List<String> returningFields = const []}) async {
var now = DateTime.now(); var now = DateTime.now();
print( print(
'_FakeExecutor received query: $query and values: $substitutionValues'); '_FakeExecutor received query: $query and values: $substitutionValues');
@ -40,6 +41,11 @@ class _FakeExecutor extends QueryExecutor {
Future<T> transaction<T>(FutureOr<T> Function(QueryExecutor) f) { Future<T> transaction<T>(FutureOr<T> Function(QueryExecutor) f) {
throw UnsupportedError('Transactions are not supported.'); throw UnsupportedError('Transactions are not supported.');
} }
final Dialect _dialect = const PostgreSQLDialect();
@override
Dialect get dialect => _dialect;
} }
@orm @orm

View file

@ -13,3 +13,4 @@ export 'src/query.dart';
export 'src/relations.dart'; export 'src/relations.dart';
export 'src/union.dart'; export 'src/union.dart';
export 'src/util.dart'; export 'src/util.dart';
export 'src/dialect/dialect.dart';

View file

@ -0,0 +1,25 @@
abstract class Dialect {
bool get cteSupport;
bool get writableCteSupport;
}
class MySQLDialect implements Dialect {
const MySQLDialect();
@override
bool get cteSupport => true;
@override
bool get writableCteSupport => false;
}
class PostgreSQLDialect implements Dialect {
const PostgreSQLDialect();
@override
bool get cteSupport => true;
@override
bool get writableCteSupport => true;
}

View file

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'package:angel3_orm/angel3_orm.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'annotations.dart'; import 'annotations.dart';
@ -298,7 +299,7 @@ abstract class Query<T, Where extends QueryWhere> extends QueryBase<T> {
} }
return ss; return ss;
})); }));
_joins.forEach((j) { for (var j in _joins) {
var c = compiledJoins[j] = j.compile(trampoline); var c = compiledJoins[j] = j.compile(trampoline);
//if (c != null) { //if (c != null) {
if (c != '') { if (c != '') {
@ -310,7 +311,7 @@ abstract class Query<T, Where extends QueryWhere> extends QueryBase<T> {
f.add('NULL'); f.add('NULL');
} }
} }
}); }
} }
if (withFields) b.write(f.join(', ')); if (withFields) b.write(f.join(', '));
fromQuery ??= tableName; fromQuery ??= tableName;
@ -354,7 +355,7 @@ abstract class Query<T, Where extends QueryWhere> extends QueryBase<T> {
if (_joins.isEmpty) { if (_joins.isEmpty) {
return executor return executor
.query(tableName, sql, substitutionValues, .query(tableName, sql, substitutionValues,
fields.map(adornWithTableName).toList()) returningFields: fields.map(adornWithTableName).toList())
.then((it) => deserializeList(it)); .then((it) => deserializeList(it));
} else { } else {
return executor.transaction((tx) async { return executor.transaction((tx) async {
@ -379,14 +380,23 @@ abstract class Query<T, Where extends QueryWhere> extends QueryBase<T> {
if (insertion == '') { if (insertion == '') {
throw StateError('No values have been specified for update.'); throw StateError('No values have been specified for update.');
} else { } else {
// TODO: How to do this in a non-Postgres DB?
var returning = fields.map(adornWithTableName).join(', ');
var sql = compile({}); var sql = compile({});
sql = 'WITH $tableName as ($insertion RETURNING $returning) ' + sql; var returningSql = sql;
if (executor.dialect is PostgreSQLDialect) {
var returning = fields.map(adornWithTableName).join(', ');
sql = 'WITH $tableName as ($insertion RETURNING $returning) ' + sql;
} else if (executor.dialect is MySQLDialect) {
sql = '$insertion';
} else {
_log.fine("Unsupported database dialect.");
}
//_log.fine("Insert Query = $sql"); _log.fine("Insert Query = $sql");
return executor.query(tableName, sql, substitutionValues).then((it) { return executor
.query(tableName, sql, substitutionValues,
returningQuery: returningSql)
.then((it) {
// Return SQL execution results // Return SQL execution results
return it.isEmpty ? Optional.empty() : deserialize(it.first); return it.isEmpty ? Optional.empty() : deserialize(it.first);
}); });

View file

@ -45,11 +45,11 @@ abstract class QueryBase<T> {
List<T> deserializeList(List<List<dynamic>> it) { List<T> deserializeList(List<List<dynamic>> it) {
var optResult = it.map(deserialize).toList(); var optResult = it.map(deserialize).toList();
var result = <T>[]; var result = <T>[];
optResult.forEach((element) { for (var element in optResult) {
element.ifPresent((item) { element.ifPresent((item) {
result.add(item); result.add(item);
}); });
}); }
return result; return result;
} }

View file

@ -1,15 +1,23 @@
import 'dart:async'; import 'dart:async';
import '../angel3_orm.dart';
/// An abstract interface that performs queries. /// An abstract interface that performs queries.
/// ///
/// This class should be implemented. /// This class should be implemented.
abstract class QueryExecutor { abstract class QueryExecutor {
const QueryExecutor(); const QueryExecutor();
Dialect get dialect;
/// Executes a single query. /// Executes a single query.
Future<List<List>> query( Future<List<List>> query(
String tableName, String query, Map<String, dynamic> substitutionValues, String tableName,
[List<String> returningFields = const []]); String query,
Map<String, dynamic> substitutionValues, {
String returningQuery = '',
List<String> returningFields = const [],
});
/// Enters a database transaction, performing the actions within, /// Enters a database transaction, performing the actions within,
/// and returning the results of [f]. /// and returning the results of [f].

View file

@ -1,10 +1,10 @@
import 'package:angel3_migration/angel3_migration.dart'; import 'package:angel3_migration/angel3_migration.dart';
import 'package:angel3_model/angel3_model.dart';
import 'package:angel3_orm/angel3_orm.dart'; import 'package:angel3_orm/angel3_orm.dart';
import 'package:angel3_orm_mysql/angel3_orm_mysql.dart'; import 'package:angel3_orm_mysql/angel3_orm_mysql.dart';
import 'package:angel3_serialize/angel3_serialize.dart'; import 'package:angel3_serialize/angel3_serialize.dart';
//import 'package:galileo_sqljocky5/sqljocky.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:galileo_sqljocky5/sqljocky.dart'; import 'package:mysql1/mysql1.dart';
import 'package:optional/optional.dart'; import 'package:optional/optional.dart';
part 'main.g.dart'; part 'main.g.dart';
@ -15,9 +15,19 @@ void main() async {
..onRecord.listen(print); ..onRecord.listen(print);
var settings = ConnectionSettings( var settings = ConnectionSettings(
db: 'angel_orm_test', user: 'angel_orm_test', password: 'angel_orm_test'); host: 'localhost',
port: 3306,
db: 'orm_test',
user: 'Test',
password: 'Test123*');
var connection = await MySqlConnection.connect(settings); var connection = await MySqlConnection.connect(settings);
var logger = Logger('angel_orm_mysql');
var results = await connection.query('select name, is_complete from todos');
//await connection.close();
print("End");
var logger = Logger('orm_mysql');
var executor = MySqlExecutor(connection, logger: logger); var executor = MySqlExecutor(connection, logger: logger);
var query = TodoQuery(); var query = TodoQuery();

View file

@ -69,7 +69,7 @@ class TodoQuery extends Query<Todo, TodoQueryWhere> {
if (row.every((x) => x == null)) return null; if (row.every((x) => x == null)) return null;
var model = Todo( var model = Todo(
id: row[0].toString(), id: row[0].toString(),
isComplete: (row[1] as bool?), isComplete: (row[1] as int?) != 0,
text: (row[2] as String?), text: (row[2] as String?),
createdAt: (row[3] as DateTime?), createdAt: (row[3] as DateTime?),
updatedAt: (row[4] as DateTime?)); updatedAt: (row[4] as DateTime?));

View file

@ -1,26 +1,33 @@
import 'dart:async'; import 'dart:async';
import 'package:angel3_orm/angel3_orm.dart'; import 'package:angel3_orm/angel3_orm.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
// import 'package:pool/pool.dart'; import 'package:mysql1/mysql1.dart';
import 'package:galileo_sqljocky5/public/connection/connection.dart';
import 'package:galileo_sqljocky5/sqljocky.dart';
class MySqlExecutor extends QueryExecutor { class MySqlExecutor extends QueryExecutor {
/// An optional [Logger] to write to. /// An optional [Logger] to write to.
final Logger? logger; final Logger? logger;
final Querier? _connection; final MySqlConnection _connection;
MySqlExecutor(this._connection, {this.logger}); MySqlExecutor(this._connection, {this.logger});
final Dialect _dialect = const MySQLDialect();
@override
Dialect get dialect => _dialect;
Future<void> close() { Future<void> close() {
return _connection.close();
/*
if (_connection is MySqlConnection) { if (_connection is MySqlConnection) {
return (_connection as MySqlConnection).close(); return (_connection as MySqlConnection).close();
} else { } else {
return Future.value(); return Future.value();
} }
*/
} }
/*
Future<Transaction> _startTransaction() { Future<Transaction> _startTransaction() {
if (_connection is Transaction) { if (_connection is Transaction) {
return Future.value(_connection as Transaction?); return Future.value(_connection as Transaction?);
@ -30,7 +37,7 @@ class MySqlExecutor extends QueryExecutor {
throw StateError('Connection must be transaction or connection'); throw StateError('Connection must be transaction or connection');
} }
} }
@override @override
Future<List<List>> query( Future<List<List>> query(
String tableName, String query, Map<String, dynamic> substitutionValues, String tableName, String query, Map<String, dynamic> substitutionValues,
@ -69,7 +76,52 @@ class MySqlExecutor extends QueryExecutor {
}); });
} }
} }
*/
@override
Future<List<List>> query(
String tableName, String query, Map<String, dynamic> substitutionValues,
{String returningQuery = '',
List<String> returningFields = const []}) async {
// Change @id -> ?
for (var name in substitutionValues.keys) {
query = query.replaceAll('@$name', '?');
}
var params = substitutionValues.values.toList();
logger?.fine('Query: $query');
logger?.fine('Values: $params');
logger?.fine('Returning Query: $returningQuery');
if (returningQuery.isNotEmpty) {
// Handle insert, update and delete
// Retrieve back the inserted record
var result = await _connection.query(query, params);
query = '$returningQuery where id = ?';
params = [result.insertId];
}
// Handle select
return _connection.query(query, params).then((results) {
return results.map((r) => r.toList()).toList();
});
}
@override
Future<T> transaction<T>(FutureOr<T> Function(QueryExecutor) f) async {
return f(this);
/*
if (_connection is! MySqlConnection) {
return await f(this);
}
await _connection.transaction((context) async {
var executor = MySqlExecutor(context, logger: logger);
});
*/
}
/*
@override @override
Future<T> transaction<T>(FutureOr<T> Function(QueryExecutor) f) async { Future<T> transaction<T>(FutureOr<T> Function(QueryExecutor) f) async {
if (_connection is Transaction) { if (_connection is Transaction) {
@ -88,4 +140,5 @@ class MySqlExecutor extends QueryExecutor {
rethrow; rethrow;
} }
} }
*/
} }

View file

@ -8,8 +8,7 @@ environment:
dependencies: dependencies:
angel3_orm: ^4.0.0 angel3_orm: ^4.0.0
logging: ^1.0.0 logging: ^1.0.0
pool: ^1.5.0 mysql1: ^0.19.0
galileo_sqljocky5: ^3.0.0
optional: ^6.0.0 optional: ^6.0.0
dev_dependencies: dev_dependencies:
angel3_migration: ^4.0.0 angel3_migration: ^4.0.0

View file

@ -10,7 +10,7 @@ void main() {
if (rec.stackTrace != null) print(rec.stackTrace); if (rec.stackTrace != null) print(rec.stackTrace);
}); });
group('postgresql', () { group('mysql', () {
group('belongsTo', group('belongsTo',
() => belongsToTests(my(['author', 'book']), close: closeMy)); () => belongsToTests(my(['author', 'book']), close: closeMy));
group( group(

View file

@ -3,7 +3,7 @@ import 'dart:io';
import 'package:angel3_orm/angel3_orm.dart'; import 'package:angel3_orm/angel3_orm.dart';
import 'package:angel3_orm_mysql/angel3_orm_mysql.dart'; import 'package:angel3_orm_mysql/angel3_orm_mysql.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:galileo_sqljocky5/sqljocky.dart'; import 'package:mysql1/mysql1.dart';
FutureOr<QueryExecutor> Function() my(Iterable<String> schemas) { FutureOr<QueryExecutor> Function() my(Iterable<String> schemas) {
return () => connectToMySql(schemas); return () => connectToMySql(schemas);
@ -14,15 +14,15 @@ Future<void> closeMy(QueryExecutor executor) =>
Future<MySqlExecutor> connectToMySql(Iterable<String> schemas) async { Future<MySqlExecutor> connectToMySql(Iterable<String> schemas) async {
var settings = ConnectionSettings( var settings = ConnectionSettings(
db: 'angel_orm_test', db: 'orm_test',
user: Platform.environment['MYSQL_USERNAME'] ?? 'angel_orm_test', host: "localhost",
password: Platform.environment['MYSQL_PASSWORD'] ?? 'angel_orm_test'); user: Platform.environment['MYSQL_USERNAME'] ?? 'Test',
password: Platform.environment['MYSQL_PASSWORD'] ?? 'Test123*');
var connection = await MySqlConnection.connect(settings); var connection = await MySqlConnection.connect(settings);
var logger = Logger('angel_orm_mysql'); var logger = Logger('orm_mysql');
for (var s in schemas) { for (var s in schemas) {
await connection await connection.query(await File('test/migrations/$s.sql').readAsString());
.execute(await File('test/migrations/$s.sql').readAsString());
} }
return MySqlExecutor(connection, logger: logger); return MySqlExecutor(connection, logger: logger);

View file

@ -1,4 +1,4 @@
CREATE TEMPORARY TABLE "authors" ( CREATE TEMPORARY TABLE authors (
id serial PRIMARY KEY, id serial PRIMARY KEY,
name varchar(255) UNIQUE NOT NULL, name varchar(255) UNIQUE NOT NULL,
created_at timestamp, created_at timestamp,

View file

@ -1,4 +1,4 @@
CREATE TEMPORARY TABLE "books" ( CREATE TEMPORARY TABLE books (
id serial PRIMARY KEY, id serial PRIMARY KEY,
author_id int NOT NULL, author_id int NOT NULL,
partner_author_id int, partner_author_id int,

View file

@ -1,4 +1,4 @@
CREATE TEMPORARY TABLE "cars" ( CREATE TEMPORARY TABLE cars (
id serial PRIMARY KEY, id serial PRIMARY KEY,
make varchar(255) NOT NULL, make varchar(255) NOT NULL,
description TEXT NOT NULL, description TEXT NOT NULL,

View file

@ -1,4 +1,4 @@
CREATE TEMPORARY TABLE "feet" ( CREATE TEMPORARY TABLE feet (
id serial PRIMARY KEY, id serial PRIMARY KEY,
leg_id int NOT NULL, leg_id int NOT NULL,
n_toes int NOT NULL, n_toes int NOT NULL,

View file

@ -1,8 +1,8 @@
CREATE TEMPORARY TABLE "fruits" ( CREATE TEMPORARY TABLE fruits (
"id" serial, id serial,
"tree_id" int, tree_id int,
"common_name" varchar, common_name varchar(255),
"created_at" timestamp, created_at timestamp,
"updated_at" timestamp, updated_at timestamp,
PRIMARY KEY(id) PRIMARY KEY(id)
); );

View file

@ -1,4 +1,4 @@
CREATE TEMPORARY TABLE "has_cars" ( CREATE TEMPORARY TABLE has_cars (
id serial PRIMARY KEY, id serial PRIMARY KEY,
type int not null, type int not null,
created_at timestamp, created_at timestamp,

View file

@ -1,4 +1,4 @@
CREATE TEMPORARY TABLE "has_maps" ( CREATE TEMPORARY TABLE has_maps (
id serial PRIMARY KEY, id serial PRIMARY KEY,
value jsonb not null, value jsonb not null,
list jsonb not null, list jsonb not null,

View file

@ -1,4 +1,4 @@
CREATE TEMPORARY TABLE "legs" ( CREATE TEMPORARY TABLE legs (
id serial PRIMARY KEY, id serial PRIMARY KEY,
name varchar(255) NOT NULL, name varchar(255) NOT NULL,
created_at timestamp, created_at timestamp,

View file

@ -1,6 +1,6 @@
CREATE TEMPORARY TABLE "numbas" ( CREATE TEMPORARY TABLE numbas (
"i" int, i int,
"parent" int references weird_joins(id), parent int,
created_at TIMESTAMP, created_at TIMESTAMP,
updated_at TIMESTAMP, updated_at TIMESTAMP,
PRIMARY KEY(i) PRIMARY KEY(i)

View file

@ -1,6 +1,6 @@
CREATE TEMPORARY TABLE "roles" ( CREATE TEMPORARY TABLE roles (
"id" serial PRIMARY KEY, id serial PRIMARY KEY,
"name" varchar(255), name varchar(255),
"created_at" timestamp, created_at timestamp,
"updated_at" timestamp updated_at timestamp
); );

View file

@ -1,7 +1,7 @@
CREATE TEMPORARY TABLE "songs" ( CREATE TEMPORARY TABLE songs (
"id" serial, id serial,
"weird_join_id" int references weird_joins(id), weird_join_id int,
"title" varchar(255), title varchar(255),
created_at TIMESTAMP, created_at TIMESTAMP,
updated_at TIMESTAMP, updated_at TIMESTAMP,
PRIMARY KEY(id) PRIMARY KEY(id)

View file

@ -1,8 +1,7 @@
CREATE TEMPORARY TABLE "trees" ( CREATE TEMPORARY TABLE trees (
"id" serial, id serial,
"rings" smallint UNIQUE, rings smallint UNIQUE,
"created_at" timestamp, created_at timestamp,
"updated_at" timestamp, updated_at timestamp,
UNIQUE(rings),
PRIMARY KEY(id) PRIMARY KEY(id)
); );

View file

@ -1,4 +1,4 @@
CREATE TEMPORARY TABLE "unorthodoxes" ( CREATE TEMPORARY TABLE unorthodoxes (
"name" varchar(255), name varchar(255),
PRIMARY KEY(name) PRIMARY KEY(name)
); );

View file

@ -1,8 +1,8 @@
CREATE TEMPORARY TABLE "users" ( CREATE TEMPORARY TABLE users (
"id" serial PRIMARY KEY, id serial PRIMARY KEY,
"username" varchar(255), username varchar(255),
"password" varchar(255), password varchar(255),
"email" varchar(255), email varchar(255),
"created_at" timestamp, created_at timestamp,
"updated_at" timestamp updated_at timestamp
); );

View file

@ -1,7 +1,7 @@
CREATE TEMPORARY TABLE "role_users" ( CREATE TEMPORARY TABLE role_users (
"id" serial PRIMARY KEY, id serial PRIMARY KEY,
"user_id" int NOT NULL, user_id int NOT NULL,
"role_id" int NOT NULL, role_id int NOT NULL,
"created_at" timestamp, created_at timestamp,
"updated_at" timestamp updated_at timestamp
); );

View file

@ -1,13 +1,15 @@
CREATE TEMPORARY TABLE "weird_joins" ( CREATE TEMPORARY TABLE weird_joins (
"id" serial, id serial,
"join_name" varchar(255) references unorthodoxes(name), join_name varchar(255),
PRIMARY KEY(id) PRIMARY KEY(id)
); );
CREATE TEMPORARY TABLE "foos" (
"bar" varchar(255), CREATE TEMPORARY TABLE foos (
bar varchar(255) not null UNIQUE,
PRIMARY KEY(bar) PRIMARY KEY(bar)
); );
CREATE TEMPORARY TABLE "foo_pivots" (
"weird_join_id" int references weird_joins(id), CREATE TEMPORARY TABLE foo_pivots (
"foo_bar" varchar(255) references foos(bar) weird_join_id int,
); foo_bar varchar(255)
);

View file

@ -16,6 +16,11 @@ class PostgreSqlExecutor extends QueryExecutor {
this.logger = logger ?? Logger('PostgreSqlExecutor'); this.logger = logger ?? Logger('PostgreSqlExecutor');
} }
final Dialect _dialect = const PostgreSQLDialect();
@override
Dialect get dialect => _dialect;
/// The underlying connection. /// The underlying connection.
PostgreSQLExecutionContext get connection => _connection; PostgreSQLExecutionContext get connection => _connection;
@ -31,8 +36,8 @@ class PostgreSqlExecutor extends QueryExecutor {
@override @override
Future<PostgreSQLResult> query( Future<PostgreSQLResult> query(
String tableName, String query, Map<String, dynamic> substitutionValues, String tableName, String query, Map<String, dynamic> substitutionValues,
[List<String>? returningFields]) { {String returningQuery = '', List<String> returningFields = const []}) {
if (returningFields != null && returningFields.isNotEmpty) { if (returningFields.isNotEmpty) {
var fields = returningFields.join(', '); var fields = returningFields.join(', ');
var returning = 'RETURNING $fields'; var returning = 'RETURNING $fields';
query = '$query $returning'; query = '$query $returning';
@ -119,6 +124,11 @@ class PostgreSqlExecutorPool extends QueryExecutor {
assert(size > 0, 'Connection pool cannot be empty.'); assert(size > 0, 'Connection pool cannot be empty.');
} }
final Dialect _dialect = const PostgreSQLDialect();
@override
Dialect get dialect => _dialect;
/// Closes all connections. /// Closes all connections.
Future close() async { Future close() async {
await _pool.close(); await _pool.close();
@ -151,11 +161,11 @@ class PostgreSqlExecutorPool extends QueryExecutor {
@override @override
Future<PostgreSQLResult> query( Future<PostgreSQLResult> query(
String tableName, String query, Map<String, dynamic> substitutionValues, String tableName, String query, Map<String, dynamic> substitutionValues,
[List<String>? returningFields]) { {String returningQuery = '', List<String> returningFields = const []}) {
return _pool.withResource(() async { return _pool.withResource(() async {
var executor = await _next(); var executor = await _next();
return executor.query( return executor.query(tableName, query, substitutionValues,
tableName, query, substitutionValues, returningFields); returningFields: returningFields);
}); });
} }

View file

@ -17,6 +17,11 @@ class PostgreSqlPoolExecutor extends QueryExecutor {
this.logger = logger ?? Logger('PostgreSqlPoolExecutor'); this.logger = logger ?? Logger('PostgreSqlPoolExecutor');
} }
final Dialect _dialect = const PostgreSQLDialect();
@override
Dialect get dialect => _dialect;
/// The underlying connection pooling. /// The underlying connection pooling.
PgPool get pool => _pool; PgPool get pool => _pool;
@ -29,7 +34,7 @@ class PostgreSqlPoolExecutor extends QueryExecutor {
@override @override
Future<PostgreSQLResult> query( Future<PostgreSQLResult> query(
String tableName, String query, Map<String, dynamic> substitutionValues, String tableName, String query, Map<String, dynamic> substitutionValues,
[List<String> returningFields = const []]) { {String returningQuery = '', List<String> returningFields = const []}) {
if (returningFields.isNotEmpty) { if (returningFields.isNotEmpty) {
var fields = returningFields.join(', '); var fields = returningFields.join(', ');
var returning = 'RETURNING $fields'; var returning = 'RETURNING $fields';