Skip to content
This repository has been archived by the owner on Aug 13, 2024. It is now read-only.

Commit

Permalink
Implement MySql
Browse files Browse the repository at this point in the history
  • Loading branch information
laingsimon committed Feb 28, 2021
1 parent 5ae55ee commit 0c241b7
Show file tree
Hide file tree
Showing 25 changed files with 651 additions and 8 deletions.
10 changes: 10 additions & 0 deletions TestScenarios/Read_AllColumnTypes/Database.MySql.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
CREATE TABLE `Person` (
Id int not null auto_increment primary key,
Name nvarchar(255) not null,
Title varchar(10),
Age int,
Price decimal(18, 2),
DoB date,
Deleted bit(1) not null default(0),
PriceIncVat decimal(18, 2) AS (`Price` * 1.2)
);
45 changes: 45 additions & 0 deletions TestScenarios/Read_AllColumnTypes/ExpectedOutput.MySql.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"tables": {
"`Person`": {
"columns": {
"Id": {
"type": "int",
"nullable": false,
"primaryKey": true
},
"Name": {
"type": "nvarchar(255)",
"nullable": false,
"primaryKey": false
},
"Title": {
"type": "varchar(10)",
"primaryKey": false
},
"Age": {
"type": "int",
"primaryKey": false
},
"Price": {
"type": "decimal(18,2)",
"primaryKey": false
},
"DoB": {
"type": "date",
"primaryKey": false
},
"Deleted": {
"type": "bit(1)",
"nullable": false,
"default": 0,
"primaryKey": false
},
"PriceIncVat": {
"expression": "`Price` * 1.2",
"primaryKey": false,
"type": "decimal(18,2)"
}
}
}
}
}
8 changes: 5 additions & 3 deletions vcdb.MySql/MySqlInstaller.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
using Microsoft.Extensions.DependencyInjection;
using vcdb.CommandLine;
using vcdb.DependencyInjection;
using vcdb.MySql.SchemaBuilding;
using vcdb.MySql.Scripting;

namespace vcdb.MySql
{
public class MySqlInstaller : IServicesInstaller
{
public void RegisterServices(IServiceCollection services, DatabaseVersion databaseVersion)
{
services.AddSingleton<IConnectionFactory, ConnectionFactory>();
//services.InNamespace<MySqlCheckConstraintRepository>().AddAsSingleton();
//services.InNamespace<MySqlCheckConstraintScriptBuilder>().AddAsSingleton();
services.InNamespace<ConnectionFactory>().AddAsSingleton();
services.InNamespace<MySqlDatabaseRepository>().AddAsSingleton();
services.InNamespace<MySqlDatabaseScriptBuilder>().AddAsSingleton();

ObjectName.Converter = new ObjectNameConverter(
delimiter: ".",
Expand Down
11 changes: 11 additions & 0 deletions vcdb.MySql/SchemaBuilding/IMySqlComputedColumnRepository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.Collections.Generic;
using System.Data.Common;
using System.Threading.Tasks;

namespace vcdb.MySql.SchemaBuilding
{
public interface IMySqlComputedColumnRepository
{
Task<Dictionary<string, string>> GetComputedColumns(DbConnection connection, ObjectName tableName);
}
}
10 changes: 10 additions & 0 deletions vcdb.MySql/SchemaBuilding/Models/ComputedColumn.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace vcdb.MySql.SchemaBuilding.Models
{
public class ComputedColumn
{
public string TABLE_SCHEMA { get; set; }
public string TABLE_NAME { get; set; }
public string COLUMN_NAME { get; set; }
public string GENERATION_EXPRESSION { get; set; }
}
}
12 changes: 12 additions & 0 deletions vcdb.MySql/SchemaBuilding/Models/DescribeOutput.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace vcdb.MySql.SchemaBuilding.Models
{
public class DescribeOutput
{
public string Field { get; set; }
public string Type { get; set; }
public string Null { get; set; }
public string Key { get; set; }
public string Default { get; set; }
public string Extra { get; set; }
}
}
9 changes: 9 additions & 0 deletions vcdb.MySql/SchemaBuilding/Models/TableCollation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace vcdb.MySql.SchemaBuilding.Models
{
public class TableCollation
{
public string column_name { get; set; }
public string charset_name { get; set; }
public string collation_name { get; set; }
}
}
16 changes: 16 additions & 0 deletions vcdb.MySql/SchemaBuilding/MySqlCheckConstraintRepository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.Collections.Generic;
using System.Data.Common;
using System.Threading.Tasks;
using vcdb.Models;
using vcdb.SchemaBuilding;

namespace vcdb.MySql.SchemaBuilding
{
public class MySqlCheckConstraintRepository : ICheckConstraintRepository
{
public Task<IEnumerable<CheckConstraintDetails>> GetCheckConstraints(DbConnection connection, ObjectName tableName)
{
return Task.FromResult<IEnumerable<CheckConstraintDetails>>(new CheckConstraintDetails[0]);
}
}
}
44 changes: 44 additions & 0 deletions vcdb.MySql/SchemaBuilding/MySqlCollationRepository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using Dapper;
using System.Collections.Generic;
using System.Data.Common;
using System.Linq;
using System.Threading.Tasks;
using vcdb.MySql.SchemaBuilding.Models;
using vcdb.SchemaBuilding;

namespace vcdb.MySql.SchemaBuilding
{
public class MySqlCollationRepository : ICollationRepository
{
public async Task<Dictionary<string, string>> GetColumnCollations(DbConnection connection, ObjectName tableName)
{
var columns = await connection.QueryAsync<TableCollation>(@"SELECT
column_name,
character_set_name,
collation_name
FROM information_schema.columns
WHERE table_name = @table_name;",
new { table_name = tableName.Name, schema = tableName.Schema });

return columns.ToDictionary(
column => column.column_name,
column => column.collation_name);
}

public async Task<string> GetDatabaseCollation(DbConnection connection)
{
var databaseCollation = await connection.QuerySingleAsync<string>("SHOW VARIABLES LIKE 'collation_database';");
if (databaseCollation == "collation_database")
{
return await GetServerCollation(connection);
}

return databaseCollation;
}

public async Task<string> GetServerCollation(DbConnection connection)
{
return await connection.QuerySingleAsync<string>("SELECT @@collation_server;");
}
}
}
112 changes: 112 additions & 0 deletions vcdb.MySql/SchemaBuilding/MySqlColumnRepository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
using Dapper;
using System.Collections.Generic;
using System.Data.Common;
using System.Linq;
using System.Threading.Tasks;
using vcdb.Models;
using vcdb.MySql.SchemaBuilding.Models;
using vcdb.SchemaBuilding;

namespace vcdb.MySql.SchemaBuilding
{
public class MySqlColumnRepository : IColumnRepository
{
/// <summary>
/// From this page: https://bugs.mysql.com/bug.php?id=69620
///
/// "nvarchar in MySQL is just an alias for VARCHAR and CHARACTER SET utf8. If you set the default charset in your DB and/or tables to utf8 you should be fine with VARCHAR in your columns"
///
/// This is the name of the utf8 collation, if it is set, then consider any varchar() or char() columns to be nvarchar() or nchar() instead
/// </summary>
private const string Utf8Collation = "utf8_general_ci";

private readonly IDescriptionRepository descriptionRepository;
private readonly ICollationRepository collationRepository;
private readonly IPrimaryKeyRepository primaryKeyRepository;

private readonly IMySqlComputedColumnRepository computedColumnRepository;

public MySqlColumnRepository(
IDescriptionRepository descriptionRepository,
ICollationRepository collationRepository,
IPrimaryKeyRepository primaryKeyRepository,
IMySqlComputedColumnRepository computedColumnRepository)
{
this.descriptionRepository = descriptionRepository;
this.collationRepository = collationRepository;
this.primaryKeyRepository = primaryKeyRepository;
this.computedColumnRepository = computedColumnRepository;
}

public async Task<Dictionary<string, ColumnDetails>> GetColumns(DbConnection connection, ObjectName tableName, Permissions tablePermissions)
{
var databaseCollation = await collationRepository.GetDatabaseCollation(connection);
var columnDescriptions = await descriptionRepository.GetColumnDescriptions(connection, tableName);
var columnCollations = await collationRepository.GetColumnCollations(connection, tableName);
var columnsInPrimaryKey = await primaryKeyRepository.GetColumnsInPrimaryKey(connection, tableName);
var computedColumns = await computedColumnRepository.GetComputedColumns(connection, tableName);

return await connection.QueryAsync<DescribeOutput>($@"
describe {tableName.Name}").ToDictionaryAsync(
column => column.Field,
column =>
{
return new ColumnDetails
{
Type = NormaliseDataType(column, columnCollations),
Nullable = OptOut.From(column.Null == "YES"),
Default = UnwrapDefinition(column.Default),
/*DefaultName = columnDefault == null || columnDefault.IsSystemNamed
? null
: columnDefault.Name,
SqlDefaultName = columnDefault?.Name,
DefaultObjectId = columnDefault?.ObjectId,*/
Description = columnDescriptions.ItemOrDefault(column.Field),
Collation = columnCollations.ItemOrDefault(column.Field) == databaseCollation || IsNationalCharacterColumn(column, columnCollations)
? null
: columnCollations.ItemOrDefault(column.Field),
PrimaryKey = columnsInPrimaryKey.Contains(column.Field),
Permissions = tablePermissions?.SubEntityPermissions?.ItemOrDefault(column.Field),
Expression = computedColumns.ItemOrDefault(column.Field)
};
});
}

private bool IsNationalCharacterColumn(DescribeOutput column, Dictionary<string, string> columnCollations)
{
var columnCollation = columnCollations.ItemOrDefault(column.Field);
var isUtf8Collation = columnCollation == Utf8Collation;
return (column.Type.StartsWith("varchar(") || column.Type.StartsWith("char(")) && isUtf8Collation;
}

private string NormaliseDataType(DescribeOutput column, Dictionary<string, string> columnCollations)
{
if (IsNationalCharacterColumn(column, columnCollations))
{
return "n" + column.Type;
}

return column.Type;
}

private object UnwrapDefinition(object definition)
{
if (definition is string stringDefinition)
{
if (string.IsNullOrEmpty(stringDefinition))
return definition;

if (stringDefinition.StartsWith("\"") && stringDefinition.EndsWith("\""))
return stringDefinition.Trim('\"');

if (int.TryParse(stringDefinition, out var intValue))
return intValue;

if (decimal.TryParse(stringDefinition, out var decimalValue))
return decimalValue;
}

return definition;
}
}
}
25 changes: 25 additions & 0 deletions vcdb.MySql/SchemaBuilding/MySqlComputedColumnRepository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Dapper;
using System.Collections.Generic;
using System.Data.Common;
using System.Linq;
using System.Threading.Tasks;
using vcdb.MySql.SchemaBuilding.Models;

namespace vcdb.MySql.SchemaBuilding
{
public class MySqlComputedColumnRepository : IMySqlComputedColumnRepository
{
public async Task<Dictionary<string, string>> GetComputedColumns(DbConnection connection, ObjectName tableName)
{
var computedColums = await connection.QueryAsync<ComputedColumn>(@"select TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, GENERATION_EXPRESSION
from INFORMATION_SCHEMA.COLUMNS
where TABLE_SCHEMA = DATABASE()
and TABLE_NAME = @tableName
and GENERATION_EXPRESSION <> ''", new { tableName = tableName.Name });

return computedColums.ToDictionary(
col => col.COLUMN_NAME,
col => col.GENERATION_EXPRESSION.UnwrapDefinition());
}
}
}
56 changes: 56 additions & 0 deletions vcdb.MySql/SchemaBuilding/MySqlDatabaseRepository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using System.Data.Common;
using System.Threading.Tasks;
using vcdb.Models;
using vcdb.SchemaBuilding;

namespace vcdb.MySql.SchemaBuilding
{
public class MySqlDatabaseRepository : IDatabaseRepository
{
private readonly ITableRepository tableRepository;
private readonly ISchemaRepository schemaRepository;
private readonly IDescriptionRepository descriptionRepository;
private readonly ICollationRepository collationRepository;
private readonly IUserRepository userRepository;
private readonly IPermissionRepository permissionRepository;
private readonly IProcedureRepository procedureRepository;

public MySqlDatabaseRepository(
ITableRepository tableRepository,
ISchemaRepository schemaRepository,
IDescriptionRepository descriptionRepository,
ICollationRepository collationRepository,
IUserRepository userRepository,
IPermissionRepository permissionRepository,
IProcedureRepository procedureRepository)
{
this.tableRepository = tableRepository;
this.schemaRepository = schemaRepository;
this.descriptionRepository = descriptionRepository;
this.collationRepository = collationRepository;
this.userRepository = userRepository;
this.permissionRepository = permissionRepository;
this.procedureRepository = procedureRepository;
}

public async Task<DatabaseDetails> GetDatabaseDetails(DbConnection connection)
{
var serverCollation = await collationRepository.GetServerCollation(connection);
var databaseCollation = await collationRepository.GetDatabaseCollation(connection);

return new DatabaseDetails
{
Tables = await tableRepository.GetTables(connection),
Schemas = await schemaRepository.GetSchemas(connection),
Description = await descriptionRepository.GetDatabaseDescription(connection),
Collation = databaseCollation == serverCollation
? null
: databaseCollation,
ServerCollation = serverCollation,
Users = await userRepository.GetUsers(connection),
Permissions = await permissionRepository.GetDatabasePermissions(connection),
Procedures = await procedureRepository.GetProcedures(connection)
};
}
}
}
Loading

0 comments on commit 0c241b7

Please sign in to comment.