diff --git a/packages/dox-app/pubspec.lock b/packages/dox-app/pubspec.lock index eb94b30..67231bf 100644 --- a/packages/dox-app/pubspec.lock +++ b/packages/dox-app/pubspec.lock @@ -426,6 +426,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + mysql1: + dependency: transitive + description: + name: mysql1 + sha256: "68aec7003d2abc85769bafa1777af3f4a390a90c31032b89636758ff8eb839e9" + url: "https://pub.dev" + source: hosted + version: "0.20.0" node_preamble: dependency: transitive description: diff --git a/packages/dox-query-builder/lib/src/drivers/db_driver.dart b/packages/dox-query-builder/lib/src/drivers/db_driver.dart index 4086995..43bda60 100644 --- a/packages/dox-query-builder/lib/src/drivers/db_driver.dart +++ b/packages/dox-query-builder/lib/src/drivers/db_driver.dart @@ -1,14 +1,19 @@ -import 'package:postgres/postgres.dart'; +enum Driver { postgres, mysql } /// interface for database driver abstract class DBDriver { + Driver getName(); + /// run query and return map result - Future execute(String query, + Future execute(String query, {Map? substitutionValues}); /// run query and return map result - Future>> mappedResultsQuery(String query, - {Map? substitutionValues}); + Future>> mappedResultsQuery( + String query, { + String? primaryKey, + Map? substitutionValues, + }); /// run query, this function do not return any value Future query(String query, {Map? substitutionValues}); diff --git a/packages/dox-query-builder/lib/src/drivers/mysql_driver.dart b/packages/dox-query-builder/lib/src/drivers/mysql_driver.dart new file mode 100644 index 0000000..f8c71fa --- /dev/null +++ b/packages/dox-query-builder/lib/src/drivers/mysql_driver.dart @@ -0,0 +1,136 @@ +import 'dart:convert'; + +import 'package:dox_query_builder/dox_query_builder.dart'; +import 'package:mysql1/mysql1.dart'; + +/// driver for postgres SQL +/// support PostgreSQLConnection and PgPool +class MysqlDriver extends DBDriver { + final MySqlConnection conn; + + /// constructor + MysqlDriver({required this.conn}); + + @override + Driver getName() { + return Driver.mysql; + } + + /// run query and return map result + @override + Future execute( + String query, { + Map? substitutionValues, + }) async { + dynamic result = + await conn.run(query, substitutionValues: substitutionValues); + return result as T; + } + + /// run query and return map result + @override + Future>> mappedResultsQuery( + String query, { + String? primaryKey, + Map? substitutionValues, + }) async { + return await conn.execute( + query, + primaryKey: primaryKey, + substitutionValues: substitutionValues, + ); + } + + /// only run query + @override + Future query( + String query, { + Map? substitutionValues, + }) async { + await conn.run(query, substitutionValues: substitutionValues); + } +} + +/// extension on mysql connection +extension MySqlConnectionExecute on MySqlConnection { + Future>> execute( + String sql, { + String? primaryKey, + Map? substitutionValues, + }) async { + Results results = await run(sql, substitutionValues: substitutionValues); + + /// if result has last insert id, it mean insert query and need to return + /// correct id + if (results.insertId != null && + results.insertId! > 0 && + primaryKey != null) { + return >[ + {primaryKey: results.insertId} + ]; + } + + /// normal get query which need to return list of map data + /// similar to postgres + return _parseResultToMap(results); + } + + Future run( + String sql, { + Map? substitutionValues, + }) async { + List params = _substitutionValuesToList(substitutionValues); + sql = sql.replaceAll(RegExp(r'@\w+'), '?'); + return await query(sql, params); + } + + List> _parseResultToMap(Results results) { + List> result = >[]; + for (ResultRow element in results) { + Map data = {}; + element.fields.forEach((String key, dynamic value) { + /// blog need to convert to string + if (value is Blob) { + data[key] = value.toString(); + } else { + data[key] = value; + } + + /// if data is json string + if (data[key].toString().startsWith('{"')) { + try { + /// convert to map + data[key] = jsonDecode(data[key]); + } catch (error) { + /// ignore error + } + } + }); + result.add(data); + } + return result; + } + + List _substitutionValuesToList(dynamic params) { + List list = []; + if (params != null && (params as Map).isNotEmpty) { + params.forEach((String key, dynamic value) { + if (value is Map) { + value = jsonEncode(value); + } else if (value is DateTime) { + value = value.toIso8601String().split('.').first; + } else if (value.toString().endsWith('Z')) { + try { + value = DateTime.parse(value).toIso8601String().split('.').first; + } catch (error) { + value = value.toString(); + } + } else { + value = value.toString(); + } + list.add(value); + }); + } + return list; + } +} diff --git a/packages/dox-query-builder/lib/src/drivers/postgres_driver.dart b/packages/dox-query-builder/lib/src/drivers/postgres_driver.dart index b73108c..2e38948 100644 --- a/packages/dox-query-builder/lib/src/drivers/postgres_driver.dart +++ b/packages/dox-query-builder/lib/src/drivers/postgres_driver.dart @@ -9,22 +9,28 @@ class PostgresDriver extends DBDriver { /// constructor PostgresDriver({required this.conn}); + @override + Driver getName() { + return Driver.postgres; + } + /// run query and return map result @override - Future execute( + Future execute( String query, { Map? substitutionValues, }) async { Result result = await conn.runTx((TxSession s) async { return await s.execute(Sql.named(query), parameters: substitutionValues); }); - return result; + return result as T; } /// run query and return map result @override Future>> mappedResultsQuery( String query, { + String? primaryKey, Map? substitutionValues, }) async { Result result = diff --git a/packages/dox-query-builder/lib/src/insert.dart b/packages/dox-query-builder/lib/src/insert.dart index 38239a3..3e3d91d 100644 --- a/packages/dox-query-builder/lib/src/insert.dart +++ b/packages/dox-query-builder/lib/src/insert.dart @@ -1,3 +1,5 @@ +import 'package:dox_query_builder/dox_query_builder.dart'; + import 'shared_mixin.dart'; mixin Insert implements SharedMixin { @@ -29,11 +31,13 @@ mixin Insert implements SharedMixin { await insertMultiple(>[data]); if (result.isNotEmpty) { Map insertedData = result.first; - int id = insertedData[primaryKey] ?? 0; + int? id = insertedData[primaryKey]; resetSubstitutionValues(); - return await queryBuilder.find(id); + if (id != null) { + return await queryBuilder.find(id); + } } - return null; + return {}; } /// insert/create multiple records @@ -59,14 +63,18 @@ mixin Insert implements SharedMixin { List ret = []; data.forEach((String key, dynamic value) { String columnKey = helper.parseColumnKey(key); - ret.add("@$columnKey"); + ret.add(columnKey); addSubstitutionValues(columnKey, value); }); values.add("(${ret.join(',')})"); } + String returningQuery = queryBuilder.dbDriver.getName() == Driver.mysql + ? '' + : 'RETURNING $primaryKey'; + String query = - "INSERT INTO $tableName (${columns.join(',')}) VALUES ${values.join(',')} RETURNING $primaryKey"; + "INSERT INTO $tableName (${columns.join(',')}) VALUES ${values.join(',')} $returningQuery"; return await helper.runQuery(query); } } diff --git a/packages/dox-query-builder/lib/src/main.dart b/packages/dox-query-builder/lib/src/main.dart index 9969072..c17d9d6 100644 --- a/packages/dox-query-builder/lib/src/main.dart +++ b/packages/dox-query-builder/lib/src/main.dart @@ -1,5 +1,5 @@ import 'package:dox_query_builder/dox_query_builder.dart'; -import 'package:postgres/postgres.dart'; +import 'package:dox_query_builder/src/drivers/mysql_driver.dart'; class SqlQueryBuilder { static final SqlQueryBuilder _singleton = SqlQueryBuilder._internal(); @@ -10,7 +10,7 @@ class SqlQueryBuilder { SqlQueryBuilder._internal(); - late DBDriver db; + late DBDriver dbDriver; bool debug = true; @@ -25,12 +25,20 @@ class SqlQueryBuilder { /// ); /// ``` static void initialize({ - required Connection database, + required dynamic database, bool debug = false, QueryPrinter? printer, + Driver driver = Driver.postgres, }) { SqlQueryBuilder sql = SqlQueryBuilder(); - sql.db = PostgresDriver(conn: database); + + if (driver == Driver.postgres) { + sql.dbDriver = PostgresDriver(conn: database); + } + if (driver == Driver.mysql) { + sql.dbDriver = MysqlDriver(conn: database); + } + sql.debug = debug; // coverage:ignore-start if (printer != null) { diff --git a/packages/dox-query-builder/lib/src/model.dart b/packages/dox-query-builder/lib/src/model.dart index 3c264cc..2aaff92 100644 --- a/packages/dox-query-builder/lib/src/model.dart +++ b/packages/dox-query-builder/lib/src/model.dart @@ -43,16 +43,17 @@ class Model extends QueryBuilder { String? updatedAtColumn = timestampsColumn['updated_at']; Map values = toMap(removeRelations: true); + DateTime currentTime = now(); if (values[primaryKey] == null) { values.removeWhere((String key, dynamic value) => value == null); if (createdAtColumn != null) { - values[createdAtColumn] = now(); + values[createdAtColumn] = currentTime; createdAt = values[createdAtColumn]; } if (updatedAtColumn != null) { - values[updatedAtColumn] = now(); + values[updatedAtColumn] = currentTime; updatedAt = values[updatedAtColumn]; } @@ -68,7 +69,7 @@ class Model extends QueryBuilder { values.remove(createdAtColumn); values.removeWhere((String key, dynamic value) => value == null); if (updatedAtColumn != null) { - values[updatedAtColumn] = now(); + values[updatedAtColumn] = currentTime; updatedAt = values[updatedAtColumn]; } diff --git a/packages/dox-query-builder/lib/src/query_builder.dart b/packages/dox-query-builder/lib/src/query_builder.dart index 14dd79e..4f22736 100644 --- a/packages/dox-query-builder/lib/src/query_builder.dart +++ b/packages/dox-query-builder/lib/src/query_builder.dart @@ -1,5 +1,4 @@ import 'package:dox_query_builder/dox_query_builder.dart'; -import 'package:postgres/postgres.dart'; import 'count.dart'; import 'delete.dart'; @@ -43,7 +42,7 @@ class QueryBuilder String primaryKey = 'id'; @override - DBDriver get db => SqlQueryBuilder().db; + DBDriver get dbDriver => SqlQueryBuilder().dbDriver; @override QueryBuilderHelper get helper => QueryBuilderHelper(this); @@ -71,8 +70,9 @@ class QueryBuilder String tableName = ''; @override - dynamic addSubstitutionValues(String key, dynamic value) { - return _substitutionValues[key] = value; + void addSubstitutionValues(String key, dynamic value) { + String index = key.replaceFirst('@', ''); + _substitutionValues[index] = value; } @override @@ -145,12 +145,12 @@ class QueryBuilder /// ``` /// var result = await QueryBuilder.query('select * from blog where id = @id', {'id' : 1}); /// - static Future query( + static Future query( String query, { Map? substitutionValues = const {}, }) { return SqlQueryBuilder() - .db - .execute(query, substitutionValues: substitutionValues); + .dbDriver + .execute(query, substitutionValues: substitutionValues); } } diff --git a/packages/dox-query-builder/lib/src/schema.dart b/packages/dox-query-builder/lib/src/schema.dart index a489227..c421068 100644 --- a/packages/dox-query-builder/lib/src/schema.dart +++ b/packages/dox-query-builder/lib/src/schema.dart @@ -44,7 +44,7 @@ class Schema { /// ``` static Future drop(String tableName) async { await SqlQueryBuilder() - .db + .dbDriver .query("DROP TABLE IF EXISTS $tableName RESTRICT"); } } diff --git a/packages/dox-query-builder/lib/src/schema/table.dart b/packages/dox-query-builder/lib/src/schema/table.dart index 4e7ef3c..09617ae 100644 --- a/packages/dox-query-builder/lib/src/schema/table.dart +++ b/packages/dox-query-builder/lib/src/schema/table.dart @@ -14,7 +14,10 @@ class Table with TableUpdate { bool debug = SqlQueryBuilder().debug; @override - DBDriver get db => SqlQueryBuilder().db; + DBDriver get dbDriver => SqlQueryBuilder().dbDriver; + + String get defaultTimestampType => + dbDriver.getName() == Driver.mysql ? 'TIMESTAMP' : 'TIMESTAMPTZ'; // coverage:ignore-start @override @@ -123,23 +126,24 @@ class Table with TableUpdate { } TableColumn timestampTz(String name) { - TableColumn col = TableColumn(name: name, type: 'TIMESTAMPTZ'); + TableColumn col = TableColumn(name: name, type: defaultTimestampType); columns.add(col); return col; } TableColumn softDeletes([dynamic name]) { TableColumn col = - TableColumn(name: name ?? 'deleted_at', type: 'TIMESTAMPTZ').nullable(); + TableColumn(name: name ?? 'deleted_at', type: defaultTimestampType) + .nullable(); columns.add(col); return col; } void timestamps() { TableColumn createdAt = - TableColumn(name: 'created_at', type: 'TIMESTAMPTZ').nullable(); + TableColumn(name: 'created_at', type: defaultTimestampType).nullable(); TableColumn updatedAt = - TableColumn(name: 'updated_at', type: 'TIMESTAMPTZ').nullable(); + TableColumn(name: 'updated_at', type: defaultTimestampType).nullable(); columns.add(createdAt); columns.add(updatedAt); } @@ -172,6 +176,6 @@ class Table with TableUpdate { if (debug) { logger.log(query); // coverage:ignore-line } - await db.mappedResultsQuery(query); + await dbDriver.mappedResultsQuery(query); } } diff --git a/packages/dox-query-builder/lib/src/schema/table.shared_mixin.dart b/packages/dox-query-builder/lib/src/schema/table.shared_mixin.dart index 9c355f8..52ba7a4 100644 --- a/packages/dox-query-builder/lib/src/schema/table.shared_mixin.dart +++ b/packages/dox-query-builder/lib/src/schema/table.shared_mixin.dart @@ -7,6 +7,6 @@ abstract class TableSharedMixin { final List columns = []; String tableName = ''; bool debug = false; - DBDriver get db; + DBDriver get dbDriver; Logger get logger; } diff --git a/packages/dox-query-builder/lib/src/schema/table.update.dart b/packages/dox-query-builder/lib/src/schema/table.update.dart index 3624861..5ff8862 100644 --- a/packages/dox-query-builder/lib/src/schema/table.update.dart +++ b/packages/dox-query-builder/lib/src/schema/table.update.dart @@ -70,13 +70,14 @@ mixin TableUpdate implements TableSharedMixin { if (debug) { logger.log(query); // coverage:ignore-line } - await db.mappedResultsQuery(query); + await dbDriver.mappedResultsQuery(query); } Future> getTableColumns() async { String query = "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '$tableName'"; - List> result = await db.mappedResultsQuery(query); + List> result = + await dbDriver.mappedResultsQuery(query); List columns = []; diff --git a/packages/dox-query-builder/lib/src/shared_mixin.dart b/packages/dox-query-builder/lib/src/shared_mixin.dart index 225c3c8..56ae18e 100644 --- a/packages/dox-query-builder/lib/src/shared_mixin.dart +++ b/packages/dox-query-builder/lib/src/shared_mixin.dart @@ -7,7 +7,7 @@ abstract class SharedMixin { QueryBuilder get queryBuilder; QueryBuilderHelper get helper; Logger get logger; - DBDriver get db; + DBDriver get dbDriver; Map get substitutionValues; String get selectQueryString; String primaryKey = 'id'; diff --git a/packages/dox-query-builder/lib/src/truncate.dart b/packages/dox-query-builder/lib/src/truncate.dart index 59865e3..9268476 100644 --- a/packages/dox-query-builder/lib/src/truncate.dart +++ b/packages/dox-query-builder/lib/src/truncate.dart @@ -1,3 +1,5 @@ +import 'package:dox_query_builder/dox_query_builder.dart'; + import 'shared_mixin.dart'; mixin Truncate implements SharedMixin { @@ -8,7 +10,11 @@ mixin Truncate implements SharedMixin { /// await Blog().truncate(resetId: true); /// ``` Future truncate({bool resetId = true}) async { - String reset = resetId ? 'RESTART IDENTITY CASCADE' : ''; + String reset = resetId + ? queryBuilder.dbDriver.getName() == Driver.postgres + ? 'RESTART IDENTITY CASCADE' + : '' + : ''; String query = "TRUNCATE TABLE $tableName $reset"; await helper.runQuery(query); } diff --git a/packages/dox-query-builder/lib/src/update.dart b/packages/dox-query-builder/lib/src/update.dart index faa091a..c363588 100644 --- a/packages/dox-query-builder/lib/src/update.dart +++ b/packages/dox-query-builder/lib/src/update.dart @@ -14,9 +14,9 @@ mixin Update implements SharedMixin { List columnToUpdate = []; data.forEach((String column, dynamic value) { - String key = helper.parseColumnKey(column); - columnToUpdate.add("$column = @$key"); - addSubstitutionValues(key, value); + String columnKey = helper.parseColumnKey(column); + columnToUpdate.add("$column = $columnKey"); + addSubstitutionValues(columnKey, value); }); q += columnToUpdate.join(','); q += helper.getCommonQuery(); diff --git a/packages/dox-query-builder/lib/src/utils/helper.dart b/packages/dox-query-builder/lib/src/utils/helper.dart index f07d120..55c1857 100644 --- a/packages/dox-query-builder/lib/src/utils/helper.dart +++ b/packages/dox-query-builder/lib/src/utils/helper.dart @@ -6,7 +6,8 @@ class QueryBuilderHelper { String parseColumnKey(String column) { String timestamp = DateTime.now().microsecondsSinceEpoch.toString(); - return "$column$timestamp".replaceAll(RegExp(r'[^\w]'), ""); + String key = "$column$timestamp".replaceAll(RegExp(r'[^\w]'), ""); + return '@$key'; } String getCommonQuery({bool isCountQuery = false}) { @@ -27,9 +28,12 @@ class QueryBuilderHelper { Future>> runQuery(String query) async { Map values = queryBuilder.substitutionValues; if (queryBuilder.shouldDebug) queryBuilder.logger.log(query, values); - DBDriver db = queryBuilder.db; query = query.replaceAll(RegExp(' +'), ' '); - return await db.mappedResultsQuery(query, substitutionValues: values); + return await queryBuilder.dbDriver.mappedResultsQuery( + query, + substitutionValues: values, + primaryKey: queryBuilder.primaryKey, + ); } List> getMapResult( diff --git a/packages/dox-query-builder/lib/src/where.dart b/packages/dox-query-builder/lib/src/where.dart index 7a31d65..fdf6473 100644 --- a/packages/dox-query-builder/lib/src/where.dart +++ b/packages/dox-query-builder/lib/src/where.dart @@ -73,9 +73,9 @@ mixin Where implements SharedMixin { } String columnKey = helper.parseColumnKey(column); if (_wheres.isEmpty) { - _wheres.add("$column $condition @$columnKey"); + _wheres.add("$column $condition $columnKey"); } else { - _wheres.add("$type $column $condition @$columnKey"); + _wheres.add("$type $column $condition $columnKey"); } addSubstitutionValues(columnKey, value); return queryBuilder; diff --git a/packages/dox-query-builder/pubspec.yaml b/packages/dox-query-builder/pubspec.yaml index 3d6481e..0531e73 100644 --- a/packages/dox-query-builder/pubspec.yaml +++ b/packages/dox-query-builder/pubspec.yaml @@ -9,6 +9,7 @@ environment: dependencies: postgres: ^3.0.5 + mysql1: ^0.20.0 dox_annotation: ^1.0.5 dev_dependencies: diff --git a/packages/dox-query-builder/test/connection.dart b/packages/dox-query-builder/test/connection.dart index c370c4a..ad8c869 100644 --- a/packages/dox-query-builder/test/connection.dart +++ b/packages/dox-query-builder/test/connection.dart @@ -1,20 +1,5 @@ -import 'dart:io'; +import 'postgres.dart'; -import 'package:postgres/postgres.dart'; - -Future poolConnection() { - String host = Platform.environment['DB_HOST'] ?? 'localhost'; - int port = int.parse(Platform.environment['PORT'] ?? '5432'); - return Connection.open( - Endpoint( - host: host, - port: port, - database: 'postgres', - username: 'postgres', - password: 'postgres', - ), - settings: PoolSettings( - sslMode: SslMode.disable, - ), - ); +Future poolConnection() { + return postgresConnection(); } diff --git a/packages/dox-query-builder/test/model_test.dart b/packages/dox-query-builder/test/model_test.dart index 1cfcdae..0472cc2 100644 --- a/packages/dox-query-builder/test/model_test.dart +++ b/packages/dox-query-builder/test/model_test.dart @@ -8,7 +8,8 @@ import 'models/blog_info/blog_info.model.dart'; void main() { group('Model |', () { setUp(() async { - SqlQueryBuilder.initialize(database: await poolConnection()); + SqlQueryBuilder.initialize( + database: await poolConnection(), driver: Driver.mysql); await Schema.create('blog', (Table table) { table.id('uid'); table.string('title'); diff --git a/packages/dox-query-builder/test/mysql.dart b/packages/dox-query-builder/test/mysql.dart new file mode 100644 index 0000000..1d0d9c3 --- /dev/null +++ b/packages/dox-query-builder/test/mysql.dart @@ -0,0 +1,16 @@ +import 'dart:io'; + +import 'package:mysql1/mysql1.dart'; + +Future mysqlConnection() { + String host = Platform.environment['DB_HOST'] ?? 'localhost'; + int port = int.parse(Platform.environment['PORT'] ?? '3306'); + ConnectionSettings settings = ConnectionSettings( + host: host, + port: port, + user: 'root', + password: 'password', + db: 'dox-framework', + ); + return MySqlConnection.connect(settings); +} diff --git a/packages/dox-query-builder/test/postgres.dart b/packages/dox-query-builder/test/postgres.dart new file mode 100644 index 0000000..797bac6 --- /dev/null +++ b/packages/dox-query-builder/test/postgres.dart @@ -0,0 +1,20 @@ +import 'dart:io'; + +import 'package:postgres/postgres.dart'; + +Future postgresConnection() { + String host = Platform.environment['DB_HOST'] ?? 'localhost'; + int port = int.parse(Platform.environment['PORT'] ?? '5432'); + return Connection.open( + Endpoint( + host: host, + port: port, + database: 'postgres', + username: 'postgres', + password: 'postgres', + ), + settings: PoolSettings( + sslMode: SslMode.disable, + ), + ); +}