From 43caae9be09431dcbafb49b6ef697feeb13e2bfa Mon Sep 17 00:00:00 2001 From: doghappy Date: Sun, 19 Jun 2022 21:32:18 +0800 Subject: [PATCH] support sqlite --- Changedb.sln | 28 + .../ChangeDB.Agent.Sqlite.csproj | 21 + src/ChangeDB.Agent.Sqlite/SqliteAgent.cs | 15 + .../SqliteConnectionProvider.cs | 13 + src/ChangeDB.Agent.Sqlite/SqliteDataDumper.cs | 21 + .../SqliteDataMigrator.cs | 97 ++ .../SqliteDatabaseManager.cs | 56 ++ .../SqliteMetadataMigrator.cs | 125 +++ .../SqliteSqlExecutor.cs | 41 + .../Utils/SqliteDataTypeMapper.cs | 155 ++++ src/ChangeDB.Agent.Sqlite/Utils/SqliteRepr.cs | 72 ++ .../Utils/SqliteSqlExpressionTranslator.cs | 156 ++++ .../Utils/SqliteUtils.cs | 139 +++ .../BaseTest.cs | 27 + ...angeDB.Agent.Sqlite.IntegrationTest.csproj | 12 + .../DatabaseEnvironment.cs | 28 + .../SqliteDataMigratorTest.cs | 166 ++++ .../SqliteDataTypeMapperTest.cs | 196 ++++ .../SqliteDatabaseManagerTest.cs | 34 + .../SqliteMetadataMigratorTest.cs | 851 ++++++++++++++++++ .../SqliteSqlExpressionTranslatorTest.cs | 167 ++++ .../ChangeDB.Agent.Sqlite.UnitTest.csproj | 12 + .../SqliteConnectionProviderTest.cs | 17 + testdb/TestDB.Sqlite/SqliteInstance.cs | 43 + testdb/TestDB.Sqlite/SqliteProvider.cs | 126 +++ testdb/TestDB.Sqlite/TestDB.Sqlite.csproj | 8 + 26 files changed, 2626 insertions(+) create mode 100644 src/ChangeDB.Agent.Sqlite/ChangeDB.Agent.Sqlite.csproj create mode 100644 src/ChangeDB.Agent.Sqlite/SqliteAgent.cs create mode 100644 src/ChangeDB.Agent.Sqlite/SqliteConnectionProvider.cs create mode 100644 src/ChangeDB.Agent.Sqlite/SqliteDataDumper.cs create mode 100644 src/ChangeDB.Agent.Sqlite/SqliteDataMigrator.cs create mode 100644 src/ChangeDB.Agent.Sqlite/SqliteDatabaseManager.cs create mode 100644 src/ChangeDB.Agent.Sqlite/SqliteMetadataMigrator.cs create mode 100644 src/ChangeDB.Agent.Sqlite/SqliteSqlExecutor.cs create mode 100644 src/ChangeDB.Agent.Sqlite/Utils/SqliteDataTypeMapper.cs create mode 100644 src/ChangeDB.Agent.Sqlite/Utils/SqliteRepr.cs create mode 100644 src/ChangeDB.Agent.Sqlite/Utils/SqliteSqlExpressionTranslator.cs create mode 100644 src/ChangeDB.Agent.Sqlite/Utils/SqliteUtils.cs create mode 100644 test/ChangeDB.Agent.Sqlite.IntegrationTest/BaseTest.cs create mode 100644 test/ChangeDB.Agent.Sqlite.IntegrationTest/ChangeDB.Agent.Sqlite.IntegrationTest.csproj create mode 100644 test/ChangeDB.Agent.Sqlite.IntegrationTest/DatabaseEnvironment.cs create mode 100644 test/ChangeDB.Agent.Sqlite.IntegrationTest/SqliteDataMigratorTest.cs create mode 100644 test/ChangeDB.Agent.Sqlite.IntegrationTest/SqliteDataTypeMapperTest.cs create mode 100644 test/ChangeDB.Agent.Sqlite.IntegrationTest/SqliteDatabaseManagerTest.cs create mode 100644 test/ChangeDB.Agent.Sqlite.IntegrationTest/SqliteMetadataMigratorTest.cs create mode 100644 test/ChangeDB.Agent.Sqlite.IntegrationTest/SqliteSqlExpressionTranslatorTest.cs create mode 100644 test/ChangeDB.Agent.Sqlite.UnitTest/ChangeDB.Agent.Sqlite.UnitTest.csproj create mode 100644 test/ChangeDB.Agent.Sqlite.UnitTest/SqliteConnectionProviderTest.cs create mode 100644 testdb/TestDB.Sqlite/SqliteInstance.cs create mode 100644 testdb/TestDB.Sqlite/SqliteProvider.cs create mode 100644 testdb/TestDB.Sqlite/TestDB.Sqlite.csproj diff --git a/Changedb.sln b/Changedb.sln index 880aea1..9b636aa 100644 --- a/Changedb.sln +++ b/Changedb.sln @@ -73,6 +73,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "subc", "subc", "{541BF71F-7 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChangeDB.SubcTest", "test\ChangeDB.SubcTest\ChangeDB.SubcTest.csproj", "{DBF6DC1D-C81F-4ECD-B7BA-E9B6D6C10AF5}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChangeDB.Agent.Sqlite.UnitTest", "test\ChangeDB.Agent.Sqlite.UnitTest\ChangeDB.Agent.Sqlite.UnitTest.csproj", "{50DFF3BD-8D1E-4F34-B7AE-115015289CC7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChangeDB.Agent.Sqlite", "src\ChangeDB.Agent.Sqlite\ChangeDB.Agent.Sqlite.csproj", "{69A3060B-BB12-4FD2-9581-36EFD60E4EBA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChangeDB.Agent.Sqlite.IntegrationTest", "test\ChangeDB.Agent.Sqlite.IntegrationTest\ChangeDB.Agent.Sqlite.IntegrationTest.csproj", "{EF399B45-AAD6-4684-8AF5-8099D20E73B8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestDB.Sqlite", "testdb\TestDB.Sqlite\TestDB.Sqlite.csproj", "{FA22E063-49A2-4DD5-8A50-5D3B3F5C0E6D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -171,6 +179,22 @@ Global {DBF6DC1D-C81F-4ECD-B7BA-E9B6D6C10AF5}.Debug|Any CPU.Build.0 = Debug|Any CPU {DBF6DC1D-C81F-4ECD-B7BA-E9B6D6C10AF5}.Release|Any CPU.ActiveCfg = Release|Any CPU {DBF6DC1D-C81F-4ECD-B7BA-E9B6D6C10AF5}.Release|Any CPU.Build.0 = Release|Any CPU + {50DFF3BD-8D1E-4F34-B7AE-115015289CC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {50DFF3BD-8D1E-4F34-B7AE-115015289CC7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {50DFF3BD-8D1E-4F34-B7AE-115015289CC7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {50DFF3BD-8D1E-4F34-B7AE-115015289CC7}.Release|Any CPU.Build.0 = Release|Any CPU + {69A3060B-BB12-4FD2-9581-36EFD60E4EBA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {69A3060B-BB12-4FD2-9581-36EFD60E4EBA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {69A3060B-BB12-4FD2-9581-36EFD60E4EBA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {69A3060B-BB12-4FD2-9581-36EFD60E4EBA}.Release|Any CPU.Build.0 = Release|Any CPU + {EF399B45-AAD6-4684-8AF5-8099D20E73B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EF399B45-AAD6-4684-8AF5-8099D20E73B8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EF399B45-AAD6-4684-8AF5-8099D20E73B8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EF399B45-AAD6-4684-8AF5-8099D20E73B8}.Release|Any CPU.Build.0 = Release|Any CPU + {FA22E063-49A2-4DD5-8A50-5D3B3F5C0E6D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FA22E063-49A2-4DD5-8A50-5D3B3F5C0E6D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FA22E063-49A2-4DD5-8A50-5D3B3F5C0E6D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FA22E063-49A2-4DD5-8A50-5D3B3F5C0E6D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -203,6 +227,10 @@ Global {51DAC443-6A8B-4400-A372-9616A153B523} = {7C2E7B66-E684-48B6-BA0C-2BCED0DE69CF} {541BF71F-7E84-4E1C-8D4A-CF708525625C} = {9D50B35B-1EAA-4620-B0A6-245456051247} {DBF6DC1D-C81F-4ECD-B7BA-E9B6D6C10AF5} = {541BF71F-7E84-4E1C-8D4A-CF708525625C} + {50DFF3BD-8D1E-4F34-B7AE-115015289CC7} = {7C2E7B66-E684-48B6-BA0C-2BCED0DE69CF} + {69A3060B-BB12-4FD2-9581-36EFD60E4EBA} = {6C7E5914-CD6B-40F1-BFD7-8FB17E3EAE11} + {EF399B45-AAD6-4684-8AF5-8099D20E73B8} = {4FA4511C-AAA5-4248-ACB4-9F328470BE8F} + {FA22E063-49A2-4DD5-8A50-5D3B3F5C0E6D} = {5909CDE9-ADA9-4344-B454-226E5C8A4C12} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {DB11BB21-3F5D-42D6-8737-F19C0666F1F8} diff --git a/src/ChangeDB.Agent.Sqlite/ChangeDB.Agent.Sqlite.csproj b/src/ChangeDB.Agent.Sqlite/ChangeDB.Agent.Sqlite.csproj new file mode 100644 index 0000000..4e5cb54 --- /dev/null +++ b/src/ChangeDB.Agent.Sqlite/ChangeDB.Agent.Sqlite.csproj @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + 6.0.5 + + + + diff --git a/src/ChangeDB.Agent.Sqlite/SqliteAgent.cs b/src/ChangeDB.Agent.Sqlite/SqliteAgent.cs new file mode 100644 index 0000000..e134912 --- /dev/null +++ b/src/ChangeDB.Agent.Sqlite/SqliteAgent.cs @@ -0,0 +1,15 @@ +namespace ChangeDB.Agent.Sqlite +{ + + public class SqliteAgent : BaseAgent + { + public override AgentSetting AgentSetting => new() + { + // there is no a limit itself for the object name. + ObjectNameMaxLength = 1024, + IdentityName = (_, table) => SqliteUtils.IdentityName(table), + DatabaseType = "sqlite" + }; + + } +} diff --git a/src/ChangeDB.Agent.Sqlite/SqliteConnectionProvider.cs b/src/ChangeDB.Agent.Sqlite/SqliteConnectionProvider.cs new file mode 100644 index 0000000..e2c22dc --- /dev/null +++ b/src/ChangeDB.Agent.Sqlite/SqliteConnectionProvider.cs @@ -0,0 +1,13 @@ +using System.Data; +using System.Data.Common; +using Microsoft.Data.Sqlite; + +namespace ChangeDB.Agent.Sqlite +{ + public class SqliteConnectionProvider : IConnectionProvider + { + public static readonly IConnectionProvider Default = new SqliteConnectionProvider(); + + public DbConnection CreateConnection(string connectionString) => new SqliteConnection(connectionString); + } +} diff --git a/src/ChangeDB.Agent.Sqlite/SqliteDataDumper.cs b/src/ChangeDB.Agent.Sqlite/SqliteDataDumper.cs new file mode 100644 index 0000000..1df5709 --- /dev/null +++ b/src/ChangeDB.Agent.Sqlite/SqliteDataDumper.cs @@ -0,0 +1,21 @@ +using ChangeDB.Dump; +using ChangeDB.Migration; + +namespace ChangeDB.Agent.Sqlite +{ + public class SqliteDataDumper : BaseDataDumper + { + public static readonly IDataDumper Default = new SqliteDataDumper(); + protected override string IdentityName(string schema, string name) + { + return SqliteUtils.IdentityName(name); + } + + protected override string ReprValue(ColumnDescriptor column, object val) + { + var dataType = SqliteDataTypeMapper.Default.ToDatabaseStoreType(column.DataType); + + return SqliteRepr.ReprConstant(val, dataType); + } + } +} diff --git a/src/ChangeDB.Agent.Sqlite/SqliteDataMigrator.cs b/src/ChangeDB.Agent.Sqlite/SqliteDataMigrator.cs new file mode 100644 index 0000000..a7bdc13 --- /dev/null +++ b/src/ChangeDB.Agent.Sqlite/SqliteDataMigrator.cs @@ -0,0 +1,97 @@ +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Threading.Tasks; +using ChangeDB.Migration; +using Microsoft.Data.Sqlite; +using static ChangeDB.Agent.Sqlite.SqliteUtils; + +namespace ChangeDB.Agent.Sqlite +{ + public class SqliteDataMigrator : BaseDataMigrator, IDataMigrator + { + public static readonly IDataMigrator Default = new SqliteDataMigrator(); + private static readonly HashSet canNotOrderByTypes = new HashSet() + { + CommonDataType.Blob, + CommonDataType.Text, + CommonDataType.NText + }; + private static string BuildColumnNames(IEnumerable names) => string.Join(", ", names.Select(p => $"[{p}]")); + + private static string BuildOrderByColumnNames(TableDescriptor table) + { + if (table.PrimaryKey?.Columns?.Count > 0) + { + return BuildColumnNames(table.PrimaryKey?.Columns.ToArray()); + } + + var names = table.Columns.Where(p => !canNotOrderByTypes.Contains(p.DataType.DbType)).Select(p => p.Name); + + return BuildColumnNames(names); + } + + public override Task CountSourceTable(TableDescriptor table, AgentContext agentContext) + { + var sql = $"select count_big(1) from {IdentityName(table)}"; + var val = agentContext.Connection.ExecuteScalar(sql); + return Task.FromResult(val); + } + + public override Task ReadSourceTable(TableDescriptor table, PageInfo pageInfo, AgentContext agentContext) + { + var sql = + $"select * from {IdentityName(table)} order by {BuildOrderByColumnNames(table)} offset {pageInfo.Offset} row fetch next {pageInfo.Limit} row only"; + return Task.FromResult(agentContext.Connection.ExecuteReaderAsTable(sql)); + } + + public override Task BeforeWriteTable(TableDescriptor tableDescriptor, AgentContext agentContext) + { + if (tableDescriptor.Columns.Any(p => p.IdentityInfo != null)) + { + agentContext.Connection.ExecuteNonQuery($"SET IDENTITY_INSERT {tableDescriptor.Name} ON"); + + } + + return Task.CompletedTask; + } + + public override Task AfterWriteTable(TableDescriptor tableDescriptor, AgentContext agentContext) + { + if (tableDescriptor.Columns.Any(p => p.IdentityInfo != null)) + { + agentContext.Connection.ExecuteNonQuery($"SET IDENTITY_INSERT {tableDescriptor.Name} OFF"); + + tableDescriptor.Columns.Where(p => p.IdentityInfo?.CurrentValue != null) + .Each((column) => + { + agentContext.Connection.ExecuteNonQuery($"DBCC CHECKIDENT ('{tableDescriptor.Name}', RESEED, {column.IdentityInfo.CurrentValue})"); + }); + } + + return Task.CompletedTask; + } + + protected override Task WriteTargetTableInDefaultMode(IAsyncEnumerable datas, TableDescriptor table, AgentContext agentContext) + { + return WriteTargetTableInBlockCopyMode(datas, table, agentContext); + } + + protected override Task WriteTargetTableInBlockCopyMode(IAsyncEnumerable datas, TableDescriptor table, AgentContext agentContext) + { + //agentContext.Connection.TryOpen(); + //var options = SqlBulkCopyOptions.Default | SqlBulkCopyOptions.KeepIdentity | SqlBulkCopyOptions.KeepNulls; + //await foreach (var datatable in datas) + //{ + // if (datatable.Rows.Count == 0) continue; + // using var bulkCopy = new SqlBulkCopy(agentContext.Connection as SqlConnection, options, null) + // { + // DestinationTableName = IdentityName(table), + // BatchSize = datatable.Rows.Count, + // }; + // bulkCopy.WriteToServer(datatable); + //} + throw new System.NotImplementedException(); + } + } +} diff --git a/src/ChangeDB.Agent.Sqlite/SqliteDatabaseManager.cs b/src/ChangeDB.Agent.Sqlite/SqliteDatabaseManager.cs new file mode 100644 index 0000000..f71de72 --- /dev/null +++ b/src/ChangeDB.Agent.Sqlite/SqliteDatabaseManager.cs @@ -0,0 +1,56 @@ +using System.Data; +using System.Data.Common; +using System.IO; +using System.Threading.Tasks; +using ChangeDB.Migration; +using Microsoft.Data.Sqlite; +using static ChangeDB.Agent.Sqlite.SqliteUtils; +namespace ChangeDB.Agent.Sqlite +{ + public class SqliteDatabaseManager : IDatabaseManager + { + public static readonly IDatabaseManager Default = new SqliteDatabaseManager(); + + public async Task CreateDatabase(string connectionString, MigrationSetting migrationSetting) + { + await CreateDatabase(connectionString); + } + + public Task DropTargetDatabaseIfExists(string connectionString, MigrationSetting migrationSetting) + { + DropDatabaseIfExists(connectionString); + return Task.CompletedTask; + } + + public Task DropDatabaseIfExists(string connectionString) + { + var fileName = GetDatabaseName(connectionString); + lock (fileName) + { + if (File.Exists(fileName)) + { + SqliteConnection.ClearAllPools(); + File.Delete(fileName); + } + } + return Task.CompletedTask; + } + + + public async Task CreateDatabase(string connection) + { + using var conn = CreateNoDatabaseConnection(connection); + await conn.OpenAsync(); + } + + private static DbConnection CreateNoDatabaseConnection(string connection) + { + var builder = new SqliteConnectionStringBuilder(connection); + return new SqliteConnection(builder.ConnectionString); + } + private static string GetDatabaseName(string connectionString) + { + return new SqliteConnectionStringBuilder(connectionString).DataSource; + } + } +} diff --git a/src/ChangeDB.Agent.Sqlite/SqliteMetadataMigrator.cs b/src/ChangeDB.Agent.Sqlite/SqliteMetadataMigrator.cs new file mode 100644 index 0000000..1e99aa9 --- /dev/null +++ b/src/ChangeDB.Agent.Sqlite/SqliteMetadataMigrator.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using ChangeDB.Migration; +using static ChangeDB.Agent.Sqlite.SqliteUtils; + +namespace ChangeDB.Agent.Sqlite +{ + public class SqliteMetadataMigrator : IMetadataMigrator + { + public static readonly IMetadataMigrator Default = new SqliteMetadataMigrator(); + + public virtual Task GetDatabaseDescriptor(AgentContext agentContext) + { + var databaseDescriptor = GetDataBaseDescriptorByEFCore(agentContext.Connection); + return Task.FromResult(databaseDescriptor); + } + + public virtual Task PreMigrateMetadata(DatabaseDescriptor databaseDescriptor, AgentContext agentContext) + { + var dataTypeMapper = SqliteDataTypeMapper.Default; + var sqlExpressionTranslator = SqliteSqlExpressionTranslator.Default; + var dbConnection = agentContext.Connection; + CreateTables(); + return Task.CompletedTask; + void CreateTables() + { + foreach (var table in databaseDescriptor.Tables) + { + var tableFullName = IdentityName(table.Name); + var columnDefines = string.Join(", ", table.Columns.Select(p => $"{BuildColumnBasicDesc(p, table.PrimaryKey)}")); + var sql = @$"CREATE TABLE {tableFullName} +( +{columnDefines} +{BuildCompositePrimaryKeyDesc(table.PrimaryKey)} +{BuildUniqueDesc(table.Uniques)} +);"; + agentContext.CreateTargetObject(sql, ObjectType.Table, tableFullName); + } + string BuildColumnBasicDesc(ColumnDescriptor column, PrimaryKeyDescriptor pk) + { + var columnName = IdentityName(column.Name); + var dataType = dataTypeMapper.ToDatabaseStoreType(column.DataType); + var nullable = column.IsNullable ? string.Empty : "NOT NULL"; + var isPrimaryKey = pk is not null && pk.Columns.Count == 1 && pk.Columns[0] == column.Name; + var primaryKey = isPrimaryKey ? "PRIMARY KEY" : string.Empty; + var increment = isPrimaryKey && column.IsIdentity && column.IdentityInfo != null ? "AUTOINCREMENT" : string.Empty; + var defaultValue = sqlExpressionTranslator.FromCommonSqlExpression(column.DefaultValue, dataType, column.DataType); + var defaultValueExpression = string.Empty; + if (!string.IsNullOrEmpty(defaultValue)) + { + var expression = defaultValue.StartsWith('(') && defaultValue.EndsWith(')') ? defaultValue : $"({defaultValue})"; + defaultValueExpression = $"DEFAULT {expression}"; + } + return $"{columnName} {dataType} {nullable} {primaryKey} {increment} {defaultValueExpression}".Trim(); + } + string BuildCompositePrimaryKeyDesc(PrimaryKeyDescriptor pk) + { + return pk is not null && pk.Columns.Count > 1 + ? $", PRIMARY KEY({pk.Columns.Select(c => IdentityName(c))})" + : string.Empty; + } + string BuildUniqueDesc(List uniques) + { + return string.Join(Environment.NewLine, uniques.Select(u => $", UNIQUE({string.Join(",", u.Columns.Select(c => IdentityName(c)))})")); + } + } + } + + public virtual Task PostMigrateMetadata(DatabaseDescriptor databaseDescriptor, AgentContext agentContext) + { + var dataTypeMapper = SqliteDataTypeMapper.Default; + var sqlExpressionTranslator = SqliteSqlExpressionTranslator.Default; + var dbConnection = agentContext.Connection; + CreateIndexs(); + //AddForeignKeys(); + + void CreateIndexs() + { + foreach (var table in databaseDescriptor.Tables) + { + var tableFullName = IdentityName(table.Name); + foreach (var index in table.Indexes) + { + var indexName = IdentityName(index.Name); + var indexColumns = string.Join(",", index.Columns.Select(p => IdentityName(p))); + if (index.IsUnique) + { + var sql = $"CREATE UNIQUE INDEX {indexName} ON {tableFullName}({indexColumns});"; + agentContext.CreateTargetObject(sql, ObjectType.UniqueIndex, indexName, tableFullName); + } + else + { + var sql = $"CREATE INDEX {indexName} ON {tableFullName}({indexColumns});"; + agentContext.CreateTargetObject(sql, ObjectType.Index, indexName, tableFullName); + } + } + } + } + + void AddForeignKeys() + { + foreach (var table in databaseDescriptor.Tables) + { + var tableFullName = IdentityName(table); + foreach (var foreignKey in table.ForeignKeys) + { + var foreignKeyName = IdentityName(foreignKey.Name); + var foreignColumns = string.Join(",", foreignKey.ColumnNames.Select(IdentityName)); + var principalColumns = string.Join(",", foreignKey.PrincipalNames.Select(p => IdentityName(p))); + var principalTable = IdentityName(foreignKey.PrincipalTable); + var sql = + $"ALTER TABLE {tableFullName} ADD CONSTRAINT {foreignKeyName}" + + $" FOREIGN KEY ({foreignColumns}) REFERENCES {principalTable}({principalColumns})"; + agentContext.CreateTargetObject(sql, ObjectType.ForeignKey, foreignKeyName, tableFullName); + + } + } + } + return Task.CompletedTask; + } + } +} diff --git a/src/ChangeDB.Agent.Sqlite/SqliteSqlExecutor.cs b/src/ChangeDB.Agent.Sqlite/SqliteSqlExecutor.cs new file mode 100644 index 0000000..8db5f8e --- /dev/null +++ b/src/ChangeDB.Agent.Sqlite/SqliteSqlExecutor.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.RegularExpressions; +using ChangeDB.Import; +using ChangeDB.Import.ContentReaders; +using ChangeDB.Import.LineHanders; +using ChangeDB.Migration; + +namespace ChangeDB.Agent.Sqlite +{ + public class SqliteSqlExecutor : BaseSqlExecutor + { + protected override IDictionary ContentReaders() + { + return new Dictionary + { + ['-'] = new CommentContentReader(), + ['\''] = new QuoteContentReader('\'', '\''), + ['"'] = new QuoteContentReader('"', '"'), + ['['] = new QuoteContentReader('[', ']') + }; + + } + + protected override ISqlLineHandler[] SqlScriptHandlers() + { + return new ISqlLineHandler[] + { + NopLineHandler.EmptyLine, + + NopLineHandler.CommentLine, + + new NopLineHandler(@"^\s*go\s*$",RegexOptions.IgnoreCase), + + new CommandLineHandler(@"^\s*(go)?\s*$",RegexOptions.IgnoreCase) + + }; + } + } +} diff --git a/src/ChangeDB.Agent.Sqlite/Utils/SqliteDataTypeMapper.cs b/src/ChangeDB.Agent.Sqlite/Utils/SqliteDataTypeMapper.cs new file mode 100644 index 0000000..05ef3b1 --- /dev/null +++ b/src/ChangeDB.Agent.Sqlite/Utils/SqliteDataTypeMapper.cs @@ -0,0 +1,155 @@ +using System; +using System.Text; +using System.Text.RegularExpressions; + +namespace ChangeDB.Agent.Sqlite +{ + internal class SqliteDataTypeMapper + { + public static SqliteDataTypeMapper Default = new SqliteDataTypeMapper(); + + public DataTypeDescriptor ToCommonDatabaseType(string storeType) + { + _ = storeType ?? throw new ArgumentNullException(nameof(storeType)); + var match = Regex.Match(storeType.ToLowerInvariant(), @"^(?[\w\s]+)(\((?\w+)(,\s*(?\w+))?\))?$"); + var type = match.Groups["name"].Value; + string arg1 = match.Groups["arg1"].Value; + string arg2 = match.Groups["arg2"].Value; + bool isMax = arg1 == "max"; + int? length = (isMax || string.IsNullOrEmpty(arg1)) ? null : int.Parse(arg1); + int? scale = string.IsNullOrEmpty(arg2) ? null : int.Parse(arg2); + return type switch + { + "integer" => DataTypeDescriptor.Int(), + "real" => DataTypeDescriptor.Double(), + "text" => DataTypeDescriptor.NText(), + "blob" => DataTypeDescriptor.Blob(), + + #region MySQL + // number + "bool" => DataTypeDescriptor.Boolean(), + "tinyint" => length == 1 ? DataTypeDescriptor.Boolean() : DataTypeDescriptor.TinyInt(), + "tinyint unsigned" => DataTypeDescriptor.TinyInt(),// TODO handle overflow + "smallint" => DataTypeDescriptor.SmallInt(), + "smallint unsigned" => DataTypeDescriptor.SmallInt(),// TODO handle overflow + "mediumint" => DataTypeDescriptor.Int(), + "mediumint unsigned" => DataTypeDescriptor.Int(), + "int" => DataTypeDescriptor.Int(), + "int unsigned" => DataTypeDescriptor.Int(), // TODO handle overflow + "bigint" => DataTypeDescriptor.BigInt(), + "bigint unsigned" => DataTypeDescriptor.BigInt(),// TODO handle overflow + "bit" => length == null || length == 1 ? DataTypeDescriptor.Boolean() : DataTypeDescriptor.BigInt(), + "decimal" => DataTypeDescriptor.Decimal(length ?? 10, scale ?? 0), + "float" => DataTypeDescriptor.Float(), + "double" => DataTypeDescriptor.Double(), + + // datetime + "timestamp" => DataTypeDescriptor.DateTime(length ?? 0), + "datetime" => DataTypeDescriptor.DateTime(length ?? 0), + "date" => DataTypeDescriptor.Date(), + "time" => DataTypeDescriptor.Time(length ?? 0), + "year" => DataTypeDescriptor.Int(), + + // text + "char" => DataTypeDescriptor.NChar(length ?? 1), + "varchar" => DataTypeDescriptor.Varchar(length ?? 1), + "tinytext" => DataTypeDescriptor.NText(), + "mediumtext" => DataTypeDescriptor.NText(), + "longtext" => DataTypeDescriptor.NText(), + "json" => DataTypeDescriptor.NText(), + + //binary + "binary" => length == 16 ? DataTypeDescriptor.Uuid() : DataTypeDescriptor.Binary(length ?? 1), + "varbinary" => DataTypeDescriptor.Varbinary(length ?? 1), + "tinyblob" => DataTypeDescriptor.Blob(), + "mediumblob" => DataTypeDescriptor.Blob(), + "longblob" => DataTypeDescriptor.Blob(), + #endregion + + #region SQL Server + "numeric" => DataTypeDescriptor.Decimal(length ?? 0, scale ?? 0), + "rowversion" => DataTypeDescriptor.Binary(8), + "uniqueidentifier" => DataTypeDescriptor.Uuid(), + "ntext" => DataTypeDescriptor.NText(), + "image" => DataTypeDescriptor.Blob(), + "smallmoney" => DataTypeDescriptor.Decimal(10, 4), + "money" => DataTypeDescriptor.Decimal(19, 4), + "nchar" => DataTypeDescriptor.NChar(length ?? 1), + "nvarchar" => isMax ? DataTypeDescriptor.NText() : DataTypeDescriptor.NVarchar(length ?? 1), + "xml" => DataTypeDescriptor.NText(), + "smalldatetime" => DataTypeDescriptor.DateTime(0), + "datetime2" => DataTypeDescriptor.DateTime(length ?? 7), + "datetimeoffset" => DataTypeDescriptor.DateTimeOffset(length ?? 7), + #endregion + + #region Postgres + "character varying" => length == null ? DataTypeDescriptor.NText() : DataTypeDescriptor.NVarchar(length.Value), + "character" => DataTypeDescriptor.NChar(length ?? 1), + "double precision" => DataTypeDescriptor.Double(), + "uuid" => DataTypeDescriptor.Uuid(), + "bytea" => DataTypeDescriptor.Blob(), + "timestamp without time zone" => DataTypeDescriptor.DateTime(length ?? 6), + "timestamp with time zone" => DataTypeDescriptor.DateTimeOffset(length ?? 6), + "time without time zone" => DataTypeDescriptor.Time(length ?? 6), + "boolean" => DataTypeDescriptor.Boolean(), + #endregion + + _ => DataTypeDescriptor.UnKnow() + }; + } + + public string ToDatabaseStoreType(DataTypeDescriptor commonDataType) + { + return commonDataType.DbType switch + { + //CommonDataType.Boolean => "BLOB", + CommonDataType.TinyInt or CommonDataType.SmallInt or CommonDataType.Int or CommonDataType.BigInt => "INTEGER", + //CommonDataType.Decimal or CommonDataType.Float or CommonDataType.Double => "REAL", + //CommonDataType.Binary or CommonDataType.Varbinary or CommonDataType.Blob or CommonDataType.Uuid => "BLOB", + //CommonDataType.Char or CommonDataType.NChar or CommonDataType.Varchar or CommonDataType.NVarchar or CommonDataType.Text or CommonDataType.NText or CommonDataType.Time or CommonDataType.Date or CommonDataType.DateTime or CommonDataType.DateTimeOffset => "TEXT", + _ => GetDefaultCase(commonDataType) + }; + + string GetDefaultCase(DataTypeDescriptor desc) + { + var builder = new StringBuilder(); + builder.Append(desc.DbType.ToString().ToLowerInvariant()); + if (desc.Arg1 is not null) + { + builder.Append('(').Append(desc.Arg1); + } + if (desc.Arg2 is not null) + { + builder.Append(',').Append(desc.Arg2); + } + if (desc.Arg1 is not null) + { + builder.Append(')'); + } + return builder.ToString(); + } + } + + private static (string Type, int? Arg1, int? Arg2) ParseStoreType(string storeType) + { + var index1 = storeType.IndexOf('('); + var index2 = storeType.IndexOf(')'); + if (index1 > 0 && index2 > 0) + { + var type = storeType[..index1] + storeType.Substring(index2 + 1); + var index3 = storeType.IndexOf(',', index1); + if (index3 > 0) + { + return (type, int.Parse(storeType.Substring(index1 + 1, index3 - index1 - 1).Trim()), + int.Parse(storeType.Substring(index3 + 1, index2 - index3 - 1).Trim())); + } + else + { + return (type, int.Parse(storeType.Substring(index1 + 1, index2 - index1 - 1).Trim()), null); + } + } + + return (storeType.ToLower(), null, null); + } + } +} diff --git a/src/ChangeDB.Agent.Sqlite/Utils/SqliteRepr.cs b/src/ChangeDB.Agent.Sqlite/Utils/SqliteRepr.cs new file mode 100644 index 0000000..7b026e2 --- /dev/null +++ b/src/ChangeDB.Agent.Sqlite/Utils/SqliteRepr.cs @@ -0,0 +1,72 @@ +using System; +using System.Data; +using System.Linq; + +namespace ChangeDB.Agent.Sqlite +{ + internal class SqliteRepr + { + public static readonly SqliteRepr Default = new SqliteRepr(); + + public static string ReprConstant(object constant, string storeType) + { + if (constant == null || Convert.IsDBNull(constant)) + { + return "null"; + } + + switch (constant) + { + case string str: + return ReprString(str, storeType); + case bool: + return Convert.ToInt32(constant).ToString(); + case double or float or long or int or short or char or byte or decimal: + return constant.ToString().TrimDecimalZeroTail(); + case Guid guid: + return $"'{guid}'"; + case byte[] bytes: + return $"x'{string.Join("", bytes.Select(p => p.ToString("X2")))}'"; + case DateTime dateTime: + return $"'{FormatDateTime(dateTime)}'"; ; + case DateTimeOffset dateTimeOffset: + return $"'{FormatDateTimeOffset(dateTimeOffset)}'"; + } + + return constant.ToString(); + } + + public static string ReprString(string input, string storeType) + { + return $"'{input.Replace("'", "''")}'"; + } + + private static string FormatDateTime(DateTime dateTime) + { + if (dateTime.TimeOfDay == TimeSpan.Zero) + { + return dateTime.ToString("yyyy-MM-dd"); + } + else if (dateTime.Millisecond == 0) + { + return dateTime.ToString("yyyy-MM-dd HH:mm:ss"); + } + else + { + return dateTime.ToString("yyyy-MM-dd HH:mm:ss.ffffff").TrimDecimalZeroTail(); + } + } + private static string FormatDateTimeOffset(DateTimeOffset dateTime) + { + if (dateTime.Millisecond == 0) + { + return dateTime.ToString("yyyy-MM-dd HH:mm:ss zzz"); + } + else + { + return dateTime.ToString("yyyy-MM-dd HH:mm:ss.ffffff").TrimDecimalZeroTail() + dateTime.ToString(" zzz"); + } + } + + } +} diff --git a/src/ChangeDB.Agent.Sqlite/Utils/SqliteSqlExpressionTranslator.cs b/src/ChangeDB.Agent.Sqlite/Utils/SqliteSqlExpressionTranslator.cs new file mode 100644 index 0000000..8ec0f04 --- /dev/null +++ b/src/ChangeDB.Agent.Sqlite/Utils/SqliteSqlExpressionTranslator.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Concurrent; +using System.Data; +using System.Linq; +using System.Text.RegularExpressions; +using ChangeDB.Descriptors; +using ChangeDB.Migration; + +namespace ChangeDB.Agent.Sqlite +{ + internal class SqliteSqlExpressionTranslator + { + public static readonly SqliteSqlExpressionTranslator Default = new SqliteSqlExpressionTranslator(); + + private static readonly ConcurrentDictionary ValueCache = + new ConcurrentDictionary(); + + + public SqlExpressionDescriptor ToCommonSqlExpression(string defaultValue, string storeType, IDbConnection conn) + { + if (defaultValue is null) + { + return default; + } + return defaultValue.ToLowerInvariant() switch + { + "current_date" or "current_time" or "current_timestamp" => new SqlExpressionDescriptor { Function = Function.Now }, + "randomblob(16)" => new SqlExpressionDescriptor { Function = Function.Uuid }, + _ => new SqlExpressionDescriptor { Constant = GetDefaultValue(defaultValue, storeType, conn) } + }; + } + + + public string FromCommonSqlExpression(SqlExpressionDescriptor sqlExpression, string storeType, DataTypeDescriptor dataTypeDescriptor) + { + if (sqlExpression?.Function != null) + { + return sqlExpression.Function.Value switch + { + Function.Now => GetNow(), + Function.Uuid => "randomblob(16)", + _ => throw new NotSupportedException($"not supported function {sqlExpression.Function.Value}") + }; + } + return SqliteRepr.ReprConstant(sqlExpression?.Constant, storeType); + + string GetNow() + { + return dataTypeDescriptor.DbType.ToString().ToLowerInvariant() switch + { + "timestamp" or "datetime" or "smalldatetime" or "datetime2" or "datetimeoffset" or "timestamp with time zone" or "timestamp without time zone" => "CURRENT_TIMESTAMP", + "date" => "CURRENT_DATE", + "time" or "time without time zone" => "CURRENT_TIME", + _ => storeType, + }; + } + } + + private static string FormatDefaultValue(CommonDataType type, string defaultValue) + { + switch (type) + { + case CommonDataType.Int: + case CommonDataType.SmallInt: + case CommonDataType.BigInt: + case CommonDataType.TinyInt: + case CommonDataType.Boolean: + return defaultValue.Trim('\''); + default: + return defaultValue; + } + } + + private static object FormatObjectValue(CommonDataType type, IDbConnection conn, string formattedValue) + { + var objectValue = conn.ExecuteScalar($"SELECT {formattedValue}"); + switch (type) + { + case CommonDataType.Text: + case CommonDataType.NText: + case CommonDataType.Char: + case CommonDataType.NChar: + case CommonDataType.Varchar: + case CommonDataType.NVarchar: + return objectValue.ToString(); + case CommonDataType.Boolean: + return BooleanParse(objectValue); + case CommonDataType.Float: + return Convert.ToSingle(objectValue); + case CommonDataType.Uuid: + return GuidParse(objectValue.ToString()); + case CommonDataType.Date: + case CommonDataType.DateTime: + return DateTime.Parse(formattedValue.Trim('\'')); + case CommonDataType.DateTimeOffset: + return DateTimeOffset.Parse(formattedValue.Trim('\'')); + default: + return objectValue; + } + } + + private static object GetDefaultValue(string defaultValue, string storeType, IDbConnection conn) + { + var type = SqliteDataTypeMapper.Default.ToCommonDatabaseType(storeType); + string formattedValue = FormatDefaultValue(type.DbType, defaultValue); + return FormatObjectValue(type.DbType, conn, formattedValue); + } + + private static bool BooleanParse(object o) + { + if (o == null) return false; + + string value = o.ToString(); + if (value == "1") return true; + if ("true".Equals(value, StringComparison.InvariantCultureIgnoreCase)) return true; + return false; + } + + private static byte[] BytesParse(string defaultValue) + { + if (!IsHexString(defaultValue)) + { + throw new ArgumentException($"'{default}' is not a hex string"); + } + string hex = defaultValue.Substring(2, defaultValue.Length - 4); + return Enumerable + .Range(0, hex.Length / 2) + .Select(x => Convert.ToByte(hex.Substring(x * 2, 2), 16)) + .ToArray(); + } + + private static bool IsHexString(string s) + { + if (s.Length < 3) return false; + if (s[0] != 'x' && s[0] != 'X') return false; + if (s[1] != '\'' && s[^1] != '\'') return false; + return true; + } + + private static Guid GuidParse(string s) + { + try + { + if (IsHexString(s)) + { + return new Guid(BytesParse(s)); + } + return Guid.Parse(s.Trim('\'')); + } + catch (Exception e) + { + throw new ArgumentException($"'{s}' is not a hex string or uuid string", e); + } + } + } +} diff --git a/src/ChangeDB.Agent.Sqlite/Utils/SqliteUtils.cs b/src/ChangeDB.Agent.Sqlite/Utils/SqliteUtils.cs new file mode 100644 index 0000000..32e9b9f --- /dev/null +++ b/src/ChangeDB.Agent.Sqlite/Utils/SqliteUtils.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using ChangeDB.Descriptors; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Scaffolding; +using Microsoft.EntityFrameworkCore.Scaffolding.Metadata; +using Microsoft.EntityFrameworkCore.Sqlite.Design.Internal; +using Microsoft.Extensions.DependencyInjection; + +namespace ChangeDB.Agent.Sqlite +{ + internal class SqliteUtils + { + public static string IdentityName(string objectName) => $"\"{objectName}\""; + + public static string IdentityName(TableDescriptor table) => IdentityName(table.Name); + + public static DatabaseDescriptor GetDataBaseDescriptorByEFCore(DbConnection dbConnection) + { + var databaseModelFactory = GetModelFactory(); + var model = databaseModelFactory.Create(dbConnection, new DatabaseModelFactoryOptions()); + return FromDatabaseModel(model, dbConnection); + } + + [SuppressMessage("Usage", "EF1001:Internal EF Core API usage.")] + private static IDatabaseModelFactory GetModelFactory() + { + var sc = new ServiceCollection(); + var designerService = new SqliteDesignTimeServices(); + sc.AddEntityFrameworkSqlite(); + designerService.ConfigureDesignTimeServices(sc); + var provider = sc.BuildServiceProvider(); + return provider.GetRequiredService(); + } + + private static DatabaseDescriptor FromDatabaseModel(DatabaseModel databaseModel, DbConnection conn) + { + return new DatabaseDescriptor + { + Tables = GetTables() + }; + List GetTables() + { + var tables = new List(); + foreach (var table in databaseModel.Tables) + { + tables.Add(new TableDescriptor + { + Name = table.Name, + Columns = table.Columns.Select(c => + { + var identity = GetIdentity(table, c); + var columnDesc = new ColumnDescriptor + { + Name = c.Name, + Collation = c.Collation, + Comment = c.Comment, + DataType = SqliteDataTypeMapper.Default.ToCommonDatabaseType(c.StoreType), + IsNullable = c.IsNullable, + IdentityInfo = identity, + IsIdentity = identity is not null, + DefaultValue = SqliteSqlExpressionTranslator.Default.ToCommonSqlExpression(c.DefaultValueSql, c.StoreType, conn) + }; + columnDesc.SetOriginStoreType(c.StoreType); + columnDesc.SetOriginDefaultValue(c.DefaultValueSql); + return columnDesc; + }).ToList(), + PrimaryKey = GetPrimaryKey(table.PrimaryKey), + Indexes = table.Indexes.Select(i => new IndexDescriptor + { + Name = i.Name, + IsUnique = i.IsUnique, + Filter = i.Filter, + Columns = i.Columns.Select(c => c.Name).ToList() + }).ToList(), + ForeignKeys = table.ForeignKeys.Select(f => new ForeignKeyDescriptor + { + Name = f.Name, + ColumnNames = f.Columns.Select(c => c.Name).ToList(), + PrincipalNames = f.PrincipalColumns.Select(c => c.Name).ToList(), + PrincipalSchema = f.PrincipalTable.Schema, + PrincipalTable = f.PrincipalTable.Name, + OnDelete = (ReferentialAction?)f.OnDelete + }).ToList(), + Uniques = table.UniqueConstraints.Select(u => new UniqueDescriptor + { + Name = u.Name, + Columns = u.Columns.Select(c => c.Name).ToList() + }).ToList() + }); + } + return tables; + } + + IdentityDescriptor GetIdentity(DatabaseTable table, DatabaseColumn column) + { + if (table.PrimaryKey != null && table.PrimaryKey.Columns.Count == 1 + && table.PrimaryKey.Columns[0] == column + && column.ValueGenerated == ValueGenerated.OnAdd + && "INTEGER".Equals(column.StoreType, StringComparison.InvariantCultureIgnoreCase)) + { + return new IdentityDescriptor + { + StartValue = 1, + IncrementBy = 1, + CurrentValue = GetSequenceCurrentValue(table.Name), + }; + } + return default; + } + + PrimaryKeyDescriptor GetPrimaryKey(DatabasePrimaryKey pk) + { + if (pk == null) + { + return null; + } + return new PrimaryKeyDescriptor + { + Name = pk.Name, + Columns = pk.Columns.Select(c => c.Name).ToList() + }; + } + + long GetSequenceCurrentValue(string table) + { + const string sql = "SELECT seq FROM sqlite_sequence where name = @table;"; + return conn.ExecuteScalar(sql, new Dictionary + { + ["table"] = table + }); + } + } + } +} diff --git a/test/ChangeDB.Agent.Sqlite.IntegrationTest/BaseTest.cs b/test/ChangeDB.Agent.Sqlite.IntegrationTest/BaseTest.cs new file mode 100644 index 0000000..1ef90e5 --- /dev/null +++ b/test/ChangeDB.Agent.Sqlite.IntegrationTest/BaseTest.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TestDB; +using Xunit; + +namespace ChangeDB.Agent.Sqlite +{ + [Collection(nameof(DatabaseEnvironment))] + public class BaseTest + { + public static IDatabase CreateDatabase(bool readOnly, params string[] initsqls) + { + return Databases.CreateDatabase(DatabaseEnvironment.DbType, readOnly, initsqls); + } + public static IDatabase CreateDatabaseFromFile(bool readOnly, string fileName) + { + return Databases.CreateDatabaseFromFile(DatabaseEnvironment.DbType, readOnly, fileName); + } + public static IDatabase RequestDatabase() + { + return Databases.RequestDatabase(DatabaseEnvironment.DbType); + } + } +} diff --git a/test/ChangeDB.Agent.Sqlite.IntegrationTest/ChangeDB.Agent.Sqlite.IntegrationTest.csproj b/test/ChangeDB.Agent.Sqlite.IntegrationTest/ChangeDB.Agent.Sqlite.IntegrationTest.csproj new file mode 100644 index 0000000..49ee8fb --- /dev/null +++ b/test/ChangeDB.Agent.Sqlite.IntegrationTest/ChangeDB.Agent.Sqlite.IntegrationTest.csproj @@ -0,0 +1,12 @@ + + + + enable + ChangeDB.Agent.Sqlite + + + + + + + diff --git a/test/ChangeDB.Agent.Sqlite.IntegrationTest/DatabaseEnvironment.cs b/test/ChangeDB.Agent.Sqlite.IntegrationTest/DatabaseEnvironment.cs new file mode 100644 index 0000000..7ed4091 --- /dev/null +++ b/test/ChangeDB.Agent.Sqlite.IntegrationTest/DatabaseEnvironment.cs @@ -0,0 +1,28 @@ +using System; +using System.Threading.Tasks; +using TestDB; +using TestDB.Sqlite; +using Xunit; + +namespace ChangeDB.Agent.Sqlite +{ + [CollectionDefinition(nameof(DatabaseEnvironment))] + public class DatabaseEnvironment : IAsyncDisposable, IDisposable, ICollectionFixture + { + public const string DbType = "sqlite"; + + public DatabaseEnvironment() + { + Databases.SetupDatabase(DbType, false); + } + public void Dispose() + { + Databases.DisposeAll(); + } + + public async ValueTask DisposeAsync() + { + await Task.Run(Dispose); + } + } +} diff --git a/test/ChangeDB.Agent.Sqlite.IntegrationTest/SqliteDataMigratorTest.cs b/test/ChangeDB.Agent.Sqlite.IntegrationTest/SqliteDataMigratorTest.cs new file mode 100644 index 0000000..a9c0260 --- /dev/null +++ b/test/ChangeDB.Agent.Sqlite.IntegrationTest/SqliteDataMigratorTest.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using System.Linq; +using System.Threading.Tasks; +using ChangeDB.Migration; +using FluentAssertions; +using Xunit; + +namespace ChangeDB.Agent.Sqlite +{ + public class SqlServerDataMigratorTest : BaseTest + { + + [Fact] + public async Task ShouldReturnTableRowCountWhenCountTable() + { + var _dataMigrator = SqliteDataMigrator.Default; + await using var database = CreateDatabase(true, + "create schema ts", + "create table ts.table1(id int primary key,nm varchar(64));", + "insert into ts.table1(id,nm) VALUES(1,'name1');", + "insert into ts.table1(id,nm) VALUES(2,'name2');", + "insert into ts.table1(id,nm) VALUES(3,'name3');" + ); + var agentContext = new AgentContext { Connection = database.Connection }; + var rows = await _dataMigrator.CountSourceTable(new TableDescriptor + { + Name = "table1", + Schema = "ts", + }, agentContext); + rows.Should().Be(3); + } + + [Fact] + [Obsolete] + public async Task ShouldReturnDataTableWhenReadTableData() + { + + var _dataMigrator = SqliteDataMigrator.Default; + await using var database = CreateDatabase(true, + "create schema ts", + "create table ts.table1(id int primary key,nm varchar(64));", + "insert into ts.table1(id,nm) VALUES(1,'name1');", + "insert into ts.table1(id,nm) VALUES(2,'name2');", + "insert into ts.table1(id,nm) VALUES(3,'name3');" + ); + var tableDesc = new TableDescriptor + { + Name = "table1", + Schema = "ts", + Columns = new List + { + new ColumnDescriptor{Name="id", DataType=DataTypeDescriptor.Int()}, + new ColumnDescriptor{Name="nm", DataType=DataTypeDescriptor.Varchar(64)} + } + }; + var context = new AgentContext + { + Connection = database.Connection, + ConnectionString = database.ConnectionString + }; + var allRows = await _dataMigrator.ReadSourceTable(tableDesc, context, new MigrationSetting()).ToItems(p => p.Rows.OfType()).ToSyncList(); + var allData = allRows.Select(p => new { Id = p.Field("id"), Name = p.Field("nm") }).ToList(); + allData.Should().BeEquivalentTo(new[] + { + new { Id = 1, Name = "name1" }, + new { Id = 2, Name = "name2" }, + new { Id = 3, Name = "name3" } + }); + } + [Fact] + [Obsolete] + public async Task ShouldSuccessWhenWriteTableData() + { + + var dataMigrator = SqliteDataMigrator.Default; + await using var database = CreateDatabase(false, + "create schema ts", + "create table ts.table1(id int primary key,nm varchar(64));" + ); + var context = new AgentContext + { + Connection = database.Connection, + ConnectionString = database.ConnectionString + }; + + var table = new DataTable(); + table.Columns.Add("id", typeof(int)); + table.Columns.Add("nm", typeof(string)); + var row = table.NewRow(); + row["id"] = 4; + row["nm"] = "name4"; + table.Rows.Add(row); + var tableDescriptor = new TableDescriptor + { + Schema = "ts", + Name = "table1", + Columns = new List + { + new ColumnDescriptor{ Name = "id", DataType = DataTypeDescriptor.Int()}, + new ColumnDescriptor{Name = "nm", DataType = DataTypeDescriptor.Varchar(64)} + } + }; + await WriteTargetTable(dataMigrator, table, tableDescriptor, context); + var data = database.Connection.ExecuteReaderAsList("select * from ts.table1"); + data.Should().BeEquivalentTo(new List> { new Tuple(4, "name4") }); + } + + [Fact] + [Obsolete] + public async Task ShouldInsertIdentityColumn() + { + var dataMigrator = SqliteDataMigrator.Default; + await using var database = CreateDatabase(false, + "create schema ts;", + "create table ts.table1(id int identity(1,1) primary key ,nm varchar(64));" + ); + var context = new AgentContext + { + Connection = database.Connection, + ConnectionString = database.ConnectionString + }; + + var table = new DataTable(); + table.Columns.Add("id", typeof(int)); + table.Columns.Add("nm", typeof(string)); + var row = table.NewRow(); + row["id"] = 1; + row["nm"] = "name1"; + table.Rows.Add(row); + var tableDescriptor = new TableDescriptor + { + Schema = "ts", + Name = "table1", + Columns = new List + { + new ColumnDescriptor + { + Name = "id", DataType = DataTypeDescriptor.Int(), IsIdentity = true, + IdentityInfo = new IdentityDescriptor + { + IsCyclic =false, + CurrentValue = 5 + } + }, + new ColumnDescriptor{Name = "nm",DataType = DataTypeDescriptor.Varchar(64)} + } + }; + await WriteTargetTable(dataMigrator, table, tableDescriptor, context); + database.Connection.ExecuteNonQuery("insert into ts.table1(nm) values('name6')"); + var data = database.Connection.ExecuteReaderAsList("select * from ts.table1"); + data.Should().BeEquivalentTo(new List> { new(1, "name1"), new(6, "name6") }); + } + + private Task WriteTargetTable(IDataMigrator dataMigrator, DataTable data, TableDescriptor tableDescriptor, + AgentContext agentContext) + { + throw new NotImplementedException(); + //await dataMigrator.BeforeWriteTargetTable(tableDescriptor, agentContext); + //await dataMigrator.WriteTargetTable(data, tableDescriptor, agentContext); + //await dataMigrator.AfterWriteTargetTable(tableDescriptor, agentContext); + } + } +} diff --git a/test/ChangeDB.Agent.Sqlite.IntegrationTest/SqliteDataTypeMapperTest.cs b/test/ChangeDB.Agent.Sqlite.IntegrationTest/SqliteDataTypeMapperTest.cs new file mode 100644 index 0000000..242d9d4 --- /dev/null +++ b/test/ChangeDB.Agent.Sqlite.IntegrationTest/SqliteDataTypeMapperTest.cs @@ -0,0 +1,196 @@ +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using ChangeDB.Migration; +using FluentAssertions; +using TestDB; +using Xunit; + +namespace ChangeDB.Agent.Sqlite +{ + public class SqliteDataTypeMapperTest : BaseTest + { + [Theory] + // SQL Server + [InlineData("bit", CommonDataType.Boolean, null, null)] + [InlineData("bit(1)", CommonDataType.Boolean, null, null)] + [InlineData("bit(2)", CommonDataType.BigInt, null, null)] + [InlineData("tinyint", CommonDataType.TinyInt, null, null)] + [InlineData("smallint", CommonDataType.SmallInt, null, null)] + [InlineData("int", CommonDataType.Int, null, null)] + [InlineData("bigint", CommonDataType.BigInt, null, null)] + [InlineData("decimal", CommonDataType.Decimal, 10, 0)] + [InlineData("decimal(20)", CommonDataType.Decimal, 20, 0)] + [InlineData("decimal(20,4)", CommonDataType.Decimal, 20, 4)] + [InlineData("numeric", CommonDataType.Decimal, 0, 0)] + [InlineData("numeric(20)", CommonDataType.Decimal, 20, 0)] + [InlineData("numeric(20,4)", CommonDataType.Decimal, 20, 4)] + [InlineData("smallmoney", CommonDataType.Decimal, 10, 4)] + [InlineData("money", CommonDataType.Decimal, 19, 4)] + [InlineData("real", CommonDataType.Double, null, null)] + [InlineData("float", CommonDataType.Float, null, null)] + [InlineData("float(24)", CommonDataType.Float, null, null)] + [InlineData("float(25)", CommonDataType.Float, null, null)] + [InlineData("float(53)", CommonDataType.Float, null, null)] + [InlineData("char(10)", CommonDataType.NChar, 10, null)] + [InlineData("char", CommonDataType.NChar, 1, null)] + [InlineData("varchar", CommonDataType.Varchar, 1, null)] + [InlineData("varchar(8000)", CommonDataType.Varchar, 8000, null)] + [InlineData("nvarchar(4000)", CommonDataType.NVarchar, 4000, null)] + [InlineData("text", CommonDataType.NText, null, null)] + [InlineData("ntext", CommonDataType.NText, null, null)] + [InlineData("xml", CommonDataType.NText, null, null)] + + [InlineData("binary", CommonDataType.Binary, 1, null)] + [InlineData("binary(10)", CommonDataType.Binary, 10, null)] + [InlineData("varbinary", CommonDataType.Varbinary, 1, null)] + [InlineData("varbinary(8000)", CommonDataType.Varbinary, 8000, null)] + [InlineData("timestamp", CommonDataType.DateTime, 0, null)] + [InlineData("rowversion", CommonDataType.Binary, 8, null)] + [InlineData("image", CommonDataType.Blob, null, null)] + + [InlineData("uniqueidentifier", CommonDataType.Uuid, null, null)] + + [InlineData("date", CommonDataType.Date, null, null)] + [InlineData("time", CommonDataType.Time, 0, null)] + [InlineData("datetime", CommonDataType.DateTime, 0, null)] + [InlineData("datetime2", CommonDataType.DateTime, 7, null)] + [InlineData("datetimeoffset", CommonDataType.DateTimeOffset, 7, null)] + + [InlineData("time(1)", CommonDataType.Time, 1, null)] + [InlineData("datetime2(1)", CommonDataType.DateTime, 1, null)] + [InlineData("datetimeoffset(1)", CommonDataType.DateTimeOffset, 1, null)] + + // MySQL + [InlineData("BOOL", CommonDataType.Boolean, null, null)] + [InlineData("TINYINT UNSIGNED", CommonDataType.TinyInt, null, null)] + [InlineData("TINYINT", CommonDataType.TinyInt, null, null)] + [InlineData("SMALLINT UNSIGNED", CommonDataType.SmallInt, null, null)] + [InlineData("MEDIUMINT UNSIGNED", CommonDataType.Int, null, null)] + [InlineData("MEDIUMINT", CommonDataType.Int, null, null)] + [InlineData("INT UNSIGNED", CommonDataType.Int, null, null)] + [InlineData("BIGINT UNSIGNED", CommonDataType.BigInt, null, null)] + [InlineData("YEAR", CommonDataType.Int, null, null)] + [InlineData("TINYTEXT", CommonDataType.NText, null, null)] + [InlineData("MEDIUMTEXT", CommonDataType.NText, null, null)] + [InlineData("LONGTEXT", CommonDataType.NText, null, null)] + [InlineData("JSON", CommonDataType.NText, null, null)] + [InlineData("BINARY(16)", CommonDataType.Uuid, null, null)] + [InlineData("TINYBLOB", CommonDataType.Blob, null, null)] + [InlineData("MEDIUMBLOB", CommonDataType.Blob, null, null)] + [InlineData("BLOB", CommonDataType.Blob, null, null)] + [InlineData("LONGBLOB", CommonDataType.Blob, null, null)] + + // Postgres + [InlineData("CHARACTER VARYING", CommonDataType.NText, null, null)] + [InlineData("CHARACTER VARYING(10)", CommonDataType.NVarchar, 10, null)] + [InlineData("CHARACTER", CommonDataType.NChar, 1, null)] + [InlineData("DOUBLE PRECISION", CommonDataType.Double, null, null)] + [InlineData("UUID", CommonDataType.Uuid, null, null)] + [InlineData("BYTEA", CommonDataType.Blob, null, null)] + [InlineData("TIMESTAMP WITHOUT TIME ZONE", CommonDataType.DateTime, 6, null)] + [InlineData("TIMESTAMP WITH TIME ZONE", CommonDataType.DateTimeOffset, 6, null)] + [InlineData("TIME WITHOUT TIME ZONE", CommonDataType.Time, 6, null)] + [InlineData("boolean", CommonDataType.Boolean, null, null)] + public async Task ShouldMapToCommonDataType(string storeType, CommonDataType commonDbType, int? arg1, int? arg2) + { + var metadataMigrator = SqliteMetadataMigrator.Default; + + using var database = CreateDatabase(false, $"create table t1(c1 {storeType})"); + var context = new AgentContext + { + ConnectionString = database.ConnectionString, + Connection = database.Connection + }; + + var databaseDesc = await metadataMigrator.GetDatabaseDescriptor(context); + var tableDesc = databaseDesc.Tables.Single(); + var columnDesc = tableDesc.Columns.Single(); + columnDesc.DataType.Should().BeEquivalentTo(new DataTypeDescriptor + { + DbType = commonDbType, + Arg1 = arg1, + Arg2 = arg2 + }); + } + + [Theory] + [ClassData(typeof(MapToTargetDataTypeTestData))] + public async Task ShouldMapToTargetDataType(DataTypeDescriptor dataTypeDescriptor, string targetStoreType) + { + var metadataMigrator = SqliteMetadataMigrator.Default; + + using var database = CreateDatabase(false); + var context = new AgentContext + { + Connection = database.Connection, + ConnectionString = database.ConnectionString + }; + var databaseDesc = new DatabaseDescriptor + { + Tables = new List + { + new TableDescriptor + { + Name = "t1", + Columns = new List + { + new ColumnDescriptor + { + Name = "c1", + DataType = dataTypeDescriptor + } + } + } + } + }; + await metadataMigrator.MigrateAllMetaData(databaseDesc, context); + + var databaseDescFromDB = await metadataMigrator.GetDatabaseDescriptor(context); + databaseDescFromDB.Tables.Single().Columns.Single().GetOriginStoreType().Should().Be(targetStoreType); + } + + class MapToTargetDataTypeTestData : List + { + public MapToTargetDataTypeTestData() + { + + Add(DataTypeDescriptor.Boolean(), "boolean"); + Add(DataTypeDescriptor.TinyInt(), "INTEGER"); + Add(DataTypeDescriptor.SmallInt(), "INTEGER"); + Add(DataTypeDescriptor.Int(), "INTEGER"); + Add(DataTypeDescriptor.BigInt(), "INTEGER"); + + + Add(DataTypeDescriptor.Uuid(), "uuid"); + Add(DataTypeDescriptor.Text(), "text"); + Add(DataTypeDescriptor.NText(), "ntext"); + Add(DataTypeDescriptor.Blob(), "blob"); + Add(DataTypeDescriptor.Float(), "float"); + Add(DataTypeDescriptor.Double(), "double"); + Add(DataTypeDescriptor.Decimal(20, 4), "decimal(20,4)"); + + Add(DataTypeDescriptor.Char(2), "char(2)"); + Add(DataTypeDescriptor.NChar(2), "nchar(2)"); + Add(DataTypeDescriptor.Varchar(2), "varchar(2)"); + Add(DataTypeDescriptor.NVarchar(2), "nvarchar(2)"); + + Add(DataTypeDescriptor.Binary(1), "binary(1)"); + Add(DataTypeDescriptor.Varbinary(10), "varbinary(10)"); + + Add(DataTypeDescriptor.Date(), "date"); + Add(DataTypeDescriptor.Time(2), "time(2)"); + Add(DataTypeDescriptor.DateTime(2), "datetime(2)"); + Add(DataTypeDescriptor.DateTimeOffset(3), "datetimeoffset(3)"); + } + + private void Add(DataTypeDescriptor descriptor, string targetStoreType) + { + this.Add(new object[] { descriptor, targetStoreType }); + } + } + } +} diff --git a/test/ChangeDB.Agent.Sqlite.IntegrationTest/SqliteDatabaseManagerTest.cs b/test/ChangeDB.Agent.Sqlite.IntegrationTest/SqliteDatabaseManagerTest.cs new file mode 100644 index 0000000..0299837 --- /dev/null +++ b/test/ChangeDB.Agent.Sqlite.IntegrationTest/SqliteDatabaseManagerTest.cs @@ -0,0 +1,34 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using ChangeDB.Migration; +using FluentAssertions; +using Xunit; + +namespace ChangeDB.Agent.Sqlite +{ + public class SqlServerDatabaseManagerTest : BaseTest + { + [Fact] + public async Task ShouldDropCurrentDatabase() + { + await using var database = CreateDatabase(false); + var databaseManager = SqliteDatabaseManager.Default; + await databaseManager.DropDatabaseIfExists(database.ConnectionString); + File.Exists(database.DatabaseName).Should().BeFalse(); + } + + [Fact] + public async Task ShouldCreateNewDatabase() + { + await using var database = RequestDatabase(); + var databaseManager = SqliteDatabaseManager.Default; + await databaseManager.CreateDatabase(database.ConnectionString); + Action action = () => + { + database.Connection.Open(); + }; + action.Should().NotThrow(); + } + } +} diff --git a/test/ChangeDB.Agent.Sqlite.IntegrationTest/SqliteMetadataMigratorTest.cs b/test/ChangeDB.Agent.Sqlite.IntegrationTest/SqliteMetadataMigratorTest.cs new file mode 100644 index 0000000..7b81eea --- /dev/null +++ b/test/ChangeDB.Agent.Sqlite.IntegrationTest/SqliteMetadataMigratorTest.cs @@ -0,0 +1,851 @@ +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Linq; +using System.Threading.Tasks; +using ChangeDB.Descriptors; +using ChangeDB.Migration; +using FluentAssertions; +using TestDB; +using Xunit; + +namespace ChangeDB.Agent.Sqlite +{ + public class SqliteMetadataMigratorTest : BaseTest, IDisposable + { + private readonly IMetadataMigrator _metadataMigrator = SqliteMetadataMigrator.Default; + private readonly AgentContext _agentContext; + private readonly DbConnection _dbConnection; + private readonly IDatabase _database; + + public SqliteMetadataMigratorTest() + { + _database = CreateDatabase(false); + _dbConnection = _database.Connection; + var agent = new SqliteAgent(); + _agentContext = new AgentContext + { + Connection = _dbConnection, + ConnectionString = _database.ConnectionString, + Agent = agent, + }; + } + + public void Dispose() + { + _database.Dispose(); + } + + #region GetDescription + [Fact] + public async Task ShouldReturnEmptyDescriptorWhenGetDatabaseDescriptionAndGivenEmptyDatabase() + { + var databaseDesc = await _metadataMigrator.GetDatabaseDescriptor(_agentContext); + databaseDesc.Should().BeEquivalentTo(new DatabaseDescriptor()); + } + [Fact] + public async Task ShouldIncludeTableInfoWhenGetDatabaseDescription() + { + _dbConnection.ExecuteNonQuery("create table table1(id int ,nm varchar(64));"); + var databaseDesc = await _metadataMigrator.GetDatabaseDescriptor(_agentContext); + databaseDesc.Tables.Should().HaveCount(1); + databaseDesc.Tables.First().Should().Match(p => p.Name == "table1"); + } + + [Fact] + + public async Task ShouldIncludeNullableColumnInfoWhenGetDatabaseDescription() + { + _dbConnection.ExecuteNonQuery("create table table1(id int, nm int NOT NULL);"); + var databaseDesc = await _metadataMigrator.GetDatabaseDescriptor(_agentContext); + var columns = databaseDesc.Tables.Single().Columns; + columns.First().IsNullable.Should().BeTrue(); + columns.Last().IsNullable.Should().BeFalse(); + + } + + [Fact] + public async Task ShouldIncludePrimaryKeyWhenGetDatabaseDescription() + { + _dbConnection.ExecuteNonQuery("create table table1(id int primary key,nm varchar(64));"); + + var databaseDesc = await _metadataMigrator.GetDatabaseDescriptor(_agentContext); + databaseDesc.Tables.Should().HaveCount(1); + databaseDesc.Tables.First().PrimaryKey.Should() + .Match(p => p.Name == string.Empty) + .And + .Match(p => p.Columns.Count == 1 && p.Columns[0] == "id"); + } + [Fact] + public async Task ShouldIncludeMultipleColumnPrimaryKeyWhenGetDatabaseDescription() + { + _dbConnection.ExecuteNonQuery( + "create table table1(id int,nm varchar(64),primary key(id,nm));"); + + var databaseDesc = await _metadataMigrator.GetDatabaseDescriptor(_agentContext); + databaseDesc.Tables.Should().HaveCount(1); + databaseDesc.Tables.First().PrimaryKey.Should() + .Match(p => p.Name == string.Empty) + .And + .Match(p => p.Columns.Count == 2 && p.Columns[0] == "id" && p.Columns[1] == "nm"); + } + + [Fact] + public async Task ShouldIncludeIndexeWhenGetDatabaseDescription() + { + _dbConnection.ExecuteNonQuery( + "create table table1(id int,nm varchar(64));", + "create index nm_index ON table1 (nm);"); + var databaseDesc = await _metadataMigrator.GetDatabaseDescriptor(_agentContext); + databaseDesc.Tables.Single().Indexes.Should() + .BeEquivalentTo(new List + { + new IndexDescriptor + { + Name = "nm_index", + Columns = new List { "nm" } + } + }); + } + + [Fact] + public async Task ShouldIncludeMultipleColumnIndexeWhenGetDatabaseDescription() + { + _dbConnection.ExecuteNonQuery( + "create table table1(id int,nm varchar(64));", + "create index id_nm_index ON table1 (id,nm);"); + var databaseDesc = await _metadataMigrator.GetDatabaseDescriptor(_agentContext); + databaseDesc.Tables.First().Indexes.Should() + .ContainSingle().And + .ContainEquivalentOf( + new IndexDescriptor + { + Name = "id_nm_index", + Columns = new List { "id", "nm" } + }); + } + + [Fact] + public async Task ShouldIncludeUniqueIndexeWhenGetDatabaseDescription() + { + _dbConnection.ExecuteNonQuery( + "create table table1(id int,nm varchar(64));", + "create unique index nm_index ON table1 (nm);"); + var databaseDesc = await _metadataMigrator.GetDatabaseDescriptor(_agentContext); + databaseDesc.Tables.First().Indexes.Should() + .ContainSingle().And + .ContainEquivalentOf( + new IndexDescriptor + { + Name = "nm_index", + IsUnique = true, + Columns = new List { "nm" } + }); + } + [Fact] + public async Task ShouldExcludePrimaryIndexesWhenGetDatabaseDescription() + { + _dbConnection.ExecuteNonQuery( + "create table table1(id int primary key,nm varchar(64));", + "create index nm_index ON table1 (nm);"); + var databaseDesc = await _metadataMigrator.GetDatabaseDescriptor(_agentContext); + databaseDesc.Tables.First().Indexes.Should() + .ContainSingle().And + .ContainEquivalentOf( + new IndexDescriptor + { + Name = "nm_index", + Columns = new List { "nm" } + }); + } + + + [Fact] + public async Task ShouldIncludeForeignKeyWhenGetDatabaseDescription() + { + _dbConnection.ExecuteNonQuery( + "create table table1(id int primary key,nm varchar(64));", + "create table table2(id int, id1 int, foreign key(id1) references table1(id));"); + var databaseDesc = await _metadataMigrator.GetDatabaseDescriptor(_agentContext); + var foreignKey = databaseDesc.Tables.Single(p => p.Name == "table2").ForeignKeys.Single(); + foreignKey.Should().BeEquivalentTo(new ForeignKeyDescriptor + { + Name = string.Empty, + OnDelete = ReferentialAction.NoAction, + PrincipalTable = "table1", + ColumnNames = new List { "id1" }, + PrincipalNames = new List { "id" } + }); + } + + [Fact] + public async Task ShouldIncludeMutilpleColumnForeignKeyWhenGetDatabaseDescription() + { + _dbConnection.ExecuteNonQuery( + "create table table1(id int,nm int,primary key(id,nm));", + "create table table2(id2 int, nm2 int, foreign key(id2, nm2) references table1(id, nm));"); + var databaseDesc = await _metadataMigrator.GetDatabaseDescriptor(_agentContext); + databaseDesc.Tables.Where(p => p.Name == "table2").Single().ForeignKeys.Should() + .ContainSingle().And.ContainEquivalentOf(new ForeignKeyDescriptor + { + Name = string.Empty, + OnDelete = ReferentialAction.NoAction, + PrincipalTable = "table1", + ColumnNames = new List { "id2", "nm2" }, + PrincipalNames = new List { "id", "nm" } + }); + } + + [Fact] + public async Task ShouldIncludeUniqueWhenGetDatabaseDescription() + { + _dbConnection.ExecuteNonQuery( + "create table table1(id int primary key,nm varchar(64) unique);"); + var databaseDesc = await _metadataMigrator.GetDatabaseDescriptor(_agentContext); + databaseDesc.Tables.Single().Uniques.Should() + .ContainSingle().Which.Columns.Should().BeEquivalentTo(new List { "nm" }); + } + + [Fact] + public async Task ShouldIncludeMutilpleColumnUniqueWhenGetDatabaseDescription() + { + _dbConnection.ExecuteNonQuery( + "create table table1(id int,nm int,unique(id,nm));"); + var databaseDesc = await _metadataMigrator.GetDatabaseDescriptor(_agentContext); + databaseDesc.Tables.Single().Uniques.Should() + .ContainSingle().Which.Columns.Should().BeEquivalentTo(new List { "id", "nm" }); + } + [Fact] + public async Task ShouldIncludeIdentityDescriptorWhenGetDatabaseDescription() + { + _dbConnection.ExecuteNonQuery( + "create table table1(id integer primary key AUTOINCREMENT, id2 varchar(64));", + "insert into table1(id2) values('test')"); + var databaseDesc = await _metadataMigrator.GetDatabaseDescriptor(_agentContext); + databaseDesc.Tables.Single(p => p.Name == "table1").Columns.Single(c => c.Name == "id").Should() + .BeEquivalentTo(new ColumnDescriptor + { + Name = "id", + DataType = DataTypeDescriptor.Int(), + IsIdentity = true, + IdentityInfo = new IdentityDescriptor + { + CurrentValue = 1, + IncrementBy = 1 + } + }); + } + [Fact] + public async Task ShouldIncludeIdentityDescriptorWithStartValueWhenGetDatabaseDescription() + { + _dbConnection.ExecuteNonQuery( + "create table table1(id INTEGER primary key AUTOINCREMENT, id2 varchar(64));", + "insert into table1(id2) values('test');", + "insert into table1(id2) values('test');"); + var databaseDesc = await _metadataMigrator.GetDatabaseDescriptor(_agentContext); + databaseDesc.Tables.Single(p => p.Name == "table1").Columns.Single(c => c.Name == "id").Should() + .BeEquivalentTo(new ColumnDescriptor + { + Name = "id", + DataType = DataTypeDescriptor.Int(), + IsIdentity = true, + IdentityInfo = new IdentityDescriptor + { + CurrentValue = 2, + IncrementBy = 1 + } + }); + } + + [Fact] + public async Task ShouldIncludeUuidColumnWhenGetDatabaseDescription() + { + _dbConnection.ExecuteNonQuery( + "create table table1(abc uniqueidentifier default '6B98F611-DEEB-4889-ABF0-0807EF11A3BF');"); + var databaseDesc = await _metadataMigrator.GetDatabaseDescriptor(_agentContext); + databaseDesc.Tables.Where(p => p.Name == "table1").Single().Should() + .BeEquivalentTo(new TableDescriptor + { + Name = "table1", + Columns = new List + { + new ColumnDescriptor + { + Name = "abc", + IsNullable = true, + DataType = DataTypeDescriptor.Uuid(), + DefaultValue = SqlExpressionDescriptor.FromConstant(Guid.Parse("6B98F611-DEEB-4889-ABF0-0807EF11A3BF")) + } + } + }); + } + [Fact] + public async Task ShouldIncludeTimestampColumnWhenGetDatabaseDescription() + { + _dbConnection.ExecuteNonQuery( + "create table table1(id datetime(6) default CURRENT_TIMESTAMP);"); + var databaseDesc = await _metadataMigrator.GetDatabaseDescriptor(_agentContext); + databaseDesc.Tables.Where(p => p.Name == "table1").Single().Should() + .BeEquivalentTo(new TableDescriptor + { + Name = "table1", + Columns = new List + { + new ColumnDescriptor{ Name="id", IsNullable=true, DataType = DataTypeDescriptor.DateTime(6), DefaultValue = SqlExpressionDescriptor.FromFunction(Function.Now)} + } + }); + } + [Fact] + public async Task ShouldIncludeDefaultValueWhenGetDatabaseDescription() + { + _dbConnection.ExecuteNonQuery( + "create table table1(id int NOT NULL default 0,nm varchar(10) default 'abc', val money(19,4) default 0);"); + var databaseDesc = await _metadataMigrator.GetDatabaseDescriptor(_agentContext); + databaseDesc.Tables.Where(p => p.Name == "table1").Single().Should() + .BeEquivalentTo(new TableDescriptor + { + Name = "table1", + Columns = new List + { + new ColumnDescriptor{ Name="id", IsNullable=false,DataType=DataTypeDescriptor.Int() }, + new ColumnDescriptor{ Name="nm", IsNullable=true, DataType = DataTypeDescriptor.NVarchar(10), DefaultValue = SqlExpressionDescriptor.FromConstant("abc")}, + new ColumnDescriptor{ Name="val", IsNullable=true, DataType = DataTypeDescriptor.Decimal(19,4), DefaultValue = SqlExpressionDescriptor.FromConstant(0m)} + } + }); + } + #endregion + + + #region MigrateMetaData + [Fact] + public async Task ShouldCreateTableEvenSchemaExistsWhenMigrateMetadata() + { + await _metadataMigrator.MigrateAllMetaData(new DatabaseDescriptor + { + Tables = new List + { + new TableDescriptor + { + Schema="ts", + Name="table1", + Columns =new List + { + new ColumnDescriptor { Name = "id", DataType = DataTypeDescriptor.Int(), IsNullable = true } + } + } + } + }, _agentContext); + var actualDatabaseDesc = await _metadataMigrator.GetDatabaseDescriptor(_agentContext); + actualDatabaseDesc.Should().BeEquivalentTo(new DatabaseDescriptor + { + Tables = new List + { + new TableDescriptor + { + Name="table1", + Columns =new List + { + new ColumnDescriptor { Name = "id", DataType = DataTypeDescriptor.Int(), IsNullable = true } + } + } + } + }); + } + [Fact] + public async Task ShouldCreateTableWhenMigrateMetadataAndNoSchema() + { + var databaseDesc = new DatabaseDescriptor() + { + Tables = new List + { + new TableDescriptor + { + Name = "table1", + Columns = new List + { + new ColumnDescriptor { Name = "id", DataType = DataTypeDescriptor.Int() } + } + } + } + }; + await _metadataMigrator.MigrateAllMetaData(databaseDesc, _agentContext); + var actualDatabaseDesc = await _metadataMigrator.GetDatabaseDescriptor(_agentContext); + actualDatabaseDesc.Should().BeEquivalentTo(databaseDesc); + } + + [Fact] + public async Task ShouldCreatePrimaryKeyWhenMigrateMetadata() + { + var databaseDesc = new DatabaseDescriptor() + { + Tables = new List + { + new TableDescriptor + { + Name = "table1", + Columns = new List + { + new ColumnDescriptor { Name = "id", DataType = DataTypeDescriptor.Int() } + }, + PrimaryKey = new PrimaryKeyDescriptor { Name = string.Empty, Columns = new List{ "id" } }, + } + } + }; + await _metadataMigrator.MigrateAllMetaData(databaseDesc, _agentContext); + var actualDatabaseDesc = await _metadataMigrator.GetDatabaseDescriptor(_agentContext); + actualDatabaseDesc.Should().BeEquivalentTo(databaseDesc); + } + + [Fact] + public async Task ShouldCreateUniqueWhenMigrateMetadata() + { + var databaseDesc = new DatabaseDescriptor + { + Tables = new List + { + new TableDescriptor + { + Name = "table1", + Columns = new List + { + new ColumnDescriptor { Name = "id", DataType = DataTypeDescriptor.Int() } + }, + Uniques = new List + { + new UniqueDescriptor { Name = string.Empty, Columns = new List{ "id" } } + } + } + } + }; + await _metadataMigrator.MigrateAllMetaData(databaseDesc, _agentContext); + var actualDatabaseDesc = await _metadataMigrator.GetDatabaseDescriptor(_agentContext); + actualDatabaseDesc.Should().BeEquivalentTo(databaseDesc); + } + + [Fact] + public async Task ShouldCreateMutilColumnUniqueWhenMigrateMetadata() + { + var databaseDesc = new DatabaseDescriptor + { + Tables = new List + { + new TableDescriptor + { + Name = "table1", + Columns = new List + { + new ColumnDescriptor { Name = "id", DataType = DataTypeDescriptor.Int() }, + new ColumnDescriptor { Name = "nm", DataType = DataTypeDescriptor.Int() } + }, + Uniques = new List + { + new UniqueDescriptor { Name = string.Empty, Columns = new List { "id", "nm" } } + } + } + } + }; + await _metadataMigrator.MigrateAllMetaData(databaseDesc, _agentContext); + var actualDatabaseDesc = await _metadataMigrator.GetDatabaseDescriptor(_agentContext); + actualDatabaseDesc.Should().BeEquivalentTo(databaseDesc); + } + + [Fact] + public async Task ShouldCreateIndexWhenMigrateMetadata() + { + var databaseDesc = new DatabaseDescriptor + { + Tables = new List + { + new TableDescriptor + { + Name = "table1", + Columns = new List + { + new ColumnDescriptor { Name = "id", DataType = DataTypeDescriptor.Int() } + }, + Indexes = new List + { + new IndexDescriptor { Name = "index_name", Columns = new List { "id" } } + } + } + } + }; + await _metadataMigrator.MigrateAllMetaData(databaseDesc, _agentContext); + var actualDatabaseDesc = await _metadataMigrator.GetDatabaseDescriptor(_agentContext); + actualDatabaseDesc.Should().BeEquivalentTo(databaseDesc); + } + + [Fact] + public async Task ShouldCreateMutilColumnIndexWhenMigrateMetadata() + { + var databaseDesc = new DatabaseDescriptor + { + Tables = new List + { + new TableDescriptor + { + Name = "table1", + Columns = new List + { + new ColumnDescriptor { Name = "id", DataType = DataTypeDescriptor.Int() }, + new ColumnDescriptor { Name = "nm", DataType = DataTypeDescriptor.Int() } + }, + Indexes = new List + { + new IndexDescriptor { Name = "index_name", Columns = new List { "id", "nm" } } + } + } + } + }; + await _metadataMigrator.MigrateAllMetaData(databaseDesc, _agentContext); + var actualDatabaseDesc = await _metadataMigrator.GetDatabaseDescriptor(_agentContext); + actualDatabaseDesc.Should().BeEquivalentTo(databaseDesc); + } + [Fact] + public async Task ShouldCreateUniqueIndexWhenMigrateMetadata() + { + var databaseDesc = new DatabaseDescriptor + { + Tables = new List + { + new TableDescriptor + { + Name = "table1", + Columns = new List + { + new ColumnDescriptor { Name = "id", DataType = DataTypeDescriptor.Int() } + }, + Indexes = new List + { + new IndexDescriptor { Name = "index_name", IsUnique = true, Columns = new List { "id" } } + } + } + } + }; + await _metadataMigrator.MigrateAllMetaData(databaseDesc, _agentContext); + var actualDatabaseDesc = await _metadataMigrator.GetDatabaseDescriptor(_agentContext); + actualDatabaseDesc.Should().BeEquivalentTo(databaseDesc); + } + [Fact] + public async Task ShouldSetColumnNullableWhenMigrateMetadata() + { + var databaseDesc = new DatabaseDescriptor + { + Tables = new List + { + new TableDescriptor + { + Name = "table1", + Columns = new List + { + new ColumnDescriptor { Name = "id", DataType = DataTypeDescriptor.Int(), IsNullable = false }, + new ColumnDescriptor { Name = "id2", DataType = DataTypeDescriptor.Int(), IsNullable = true } + } + } + } + }; + await _metadataMigrator.MigrateAllMetaData(databaseDesc, _agentContext); + var actualDatabaseDesc = await _metadataMigrator.GetDatabaseDescriptor(_agentContext); + actualDatabaseDesc.Should().BeEquivalentTo(databaseDesc); + } + + /* + [Fact] + public async Task ShouldCreatForgienKeysWhenMigrateMetadata() + { + var databaseDesc = new DatabaseDescriptor() + { + Tables = new List + { + new TableDescriptor + { + Schema = "ts", + Name = "table1", + Columns = new List + { + new ColumnDescriptor { Name = "id", DataType = DataTypeDescriptor.Int()}, + }, + Uniques = new List + { + new UniqueDescriptor { Name = "unique_table1_id", Columns = new List { "id" } } + } + }, + new TableDescriptor + { + Schema = "ts2", + Name = "table2", + Columns =new List + { + new ColumnDescriptor { Name = "id2", DataType = DataTypeDescriptor.Int()}, + }, + ForeignKeys = new List + { + new ForeignKeyDescriptor + { + Name = "foreign_key", + PrincipalSchema = "ts", + PrincipalTable = "table1", + PrincipalNames = new List { "id" }, + ColumnNames = new List { "id2" }, + OnDelete = ReferentialAction.NoAction + } + } + } + } + }; + await _metadataMigrator.MigrateAllMetaData(databaseDesc, _agentContext); + var actualDatabaseDesc = await _metadataMigrator.GetDatabaseDescriptor(_agentContext); + actualDatabaseDesc.Should().BeEquivalentTo(databaseDesc); + } + + [Fact] + public async Task ShouldCreatMutilColumnForgienKeysWhenMigrateMetadata() + { + var databaseDesc = new DatabaseDescriptor() + { + Tables = new List + { + new TableDescriptor + { + Schema="ts", + Name="table1", + Columns =new List + { + new ColumnDescriptor { Name="id",DataType=DataTypeDescriptor.Int()}, + new ColumnDescriptor { Name="nm",DataType=DataTypeDescriptor.Int()}, + }, + Uniques = new List + { + new UniqueDescriptor { Name="unique_table1_id", Columns = new List{ "id","nm"} } + } + }, + new TableDescriptor + { + Schema="ts2", + Name="table2", + Columns =new List + { + new ColumnDescriptor { Name="id2",DataType=DataTypeDescriptor.Int()}, + new ColumnDescriptor { Name="nm2",DataType=DataTypeDescriptor.Int()}, + }, + ForeignKeys = new List + { + new ForeignKeyDescriptor + { + Name="foreign_key", + PrincipalSchema="ts", + PrincipalTable="table1", + PrincipalNames= new List {"id","nm" }, + ColumnNames= new List { "id2","nm2"}, + OnDelete = ReferentialAction.NoAction + } + } + } + } + }; + await _metadataMigrator.MigrateAllMetaData(databaseDesc, _agentContext); + var actualDatabaseDesc = await _metadataMigrator.GetDatabaseDescriptor(_agentContext); + actualDatabaseDesc.Should().BeEquivalentTo(databaseDesc); + } + */ + + [Fact] + public async Task ShouldSetColumnDefaultValueWhenMigrateMetadata() + { + var databaseDesc = new DatabaseDescriptor() + { + Tables = new List + { + new TableDescriptor + { + Name = "table1", + Columns = new List + { + new ColumnDescriptor { Name = "id", DataType = DataTypeDescriptor.Int(), DefaultValue = SqlExpressionDescriptor.FromConstant(1) }, + new ColumnDescriptor { Name = "nm", DataType = DataTypeDescriptor.Varchar(10), DefaultValue = SqlExpressionDescriptor.FromConstant("abc") }, + new ColumnDescriptor { Name = "used", DataType = DataTypeDescriptor.Boolean(), DefaultValue = SqlExpressionDescriptor.FromConstant(true) }, + new ColumnDescriptor { Name = "rid", DataType = DataTypeDescriptor.Uuid(), DefaultValue = SqlExpressionDescriptor.FromFunction(Function.Uuid) }, + new ColumnDescriptor { Name = "createtime", DataType = DataTypeDescriptor.DateTime(3),DefaultValue = SqlExpressionDescriptor.FromFunction(Function.Now) }, + }, + PrimaryKey = new PrimaryKeyDescriptor{ Name = String.Empty, Columns = new List { "id" }} + } + } + }; + await _metadataMigrator.MigrateAllMetaData(databaseDesc, _agentContext); + var actualDatabaseDesc = await _metadataMigrator.GetDatabaseDescriptor(_agentContext); + actualDatabaseDesc.Should().BeEquivalentTo(databaseDesc); + } + + [Fact] + public async Task ShouldNotMapIdentityIfColumnIsNotPrimaryKeyWhenMigrateMetadata() + { + var databaseDesc = new DatabaseDescriptor + { + Tables = new List + { + new TableDescriptor + { + Name = "table1", + Columns =new List + { + new ColumnDescriptor + { + Name = "id", + DataType = DataTypeDescriptor.Int(), + IsIdentity = true, + IdentityInfo = new IdentityDescriptor() + } + } + } + } + }; + await _metadataMigrator.MigrateAllMetaData(databaseDesc, _agentContext); + var actualDatabaseDesc = await _metadataMigrator.GetDatabaseDescriptor(_agentContext); + actualDatabaseDesc.Should().BeEquivalentTo(new DatabaseDescriptor + { + Tables = new List + { + new TableDescriptor + { + Name = "table1", + Columns =new List + { + new ColumnDescriptor + { + Name = "id", + DataType = DataTypeDescriptor.Int(), + IsIdentity = false, + } + } + } + } + }); + } + + [Fact] + public async Task ShouldMapIdentityIfColumnIsPrimaryKeyWhenMigrateMetadata() + { + await _metadataMigrator.MigrateAllMetaData(new DatabaseDescriptor + { + Tables = new List + { + new TableDescriptor + { + Name = "table1", + PrimaryKey = new PrimaryKeyDescriptor + { + Name = string.Empty, + Columns = new List { "id" } + }, + Columns =new List + { + new ColumnDescriptor + { + Name = "id", + DataType = DataTypeDescriptor.Int(), + IsIdentity = true, + IdentityInfo = new IdentityDescriptor() + } + } + } + } + }, _agentContext); + var actualDatabaseDesc = await _metadataMigrator.GetDatabaseDescriptor(_agentContext); + actualDatabaseDesc.Should().BeEquivalentTo(new DatabaseDescriptor + { + Tables = new List + { + new TableDescriptor + { + Name = "table1", + PrimaryKey = new PrimaryKeyDescriptor + { + Name = string.Empty, + Columns = new List { "id" } + }, + Columns =new List + { + new ColumnDescriptor + { + Name = "id", + DataType = DataTypeDescriptor.Int(), + IsIdentity = true, + IdentityInfo = new IdentityDescriptor + { + CurrentValue = 0 + } + } + } + } + } + }); + } + + [Fact] + public async Task ShouldMapIdentityWhenMigrateMetadataAndWithFullArguments() + { + await _metadataMigrator.MigrateAllMetaData(new DatabaseDescriptor + { + Tables = new List + { + new TableDescriptor + { + Name = "table1", + PrimaryKey = new PrimaryKeyDescriptor + { + Name = string.Empty, + Columns = new List { "id" } + }, + Columns =new List + { + new ColumnDescriptor + { + Name = "id", + DataType = DataTypeDescriptor.Int(), + IsIdentity = true, + IdentityInfo = new IdentityDescriptor + { + CurrentValue = 1, + StartValue = 5, + IncrementBy = 2 + } + } + } + } + } + }, _agentContext); + var actualDatabaseDesc = await _metadataMigrator.GetDatabaseDescriptor(_agentContext); + actualDatabaseDesc.Should().BeEquivalentTo(new DatabaseDescriptor + { + Tables = new List + { + new TableDescriptor + { + Name = "table1", + PrimaryKey = new PrimaryKeyDescriptor + { + Name = string.Empty, + Columns = new List { "id" } + }, + Columns =new List + { + new ColumnDescriptor + { + Name = "id", + DataType = DataTypeDescriptor.Int(), + IsIdentity = true, + IdentityInfo = new IdentityDescriptor + { + CurrentValue = 0, + StartValue = 1, + IncrementBy = 1 + } + } + } + } + } + }); + } + #endregion + } +} diff --git a/test/ChangeDB.Agent.Sqlite.IntegrationTest/SqliteSqlExpressionTranslatorTest.cs b/test/ChangeDB.Agent.Sqlite.IntegrationTest/SqliteSqlExpressionTranslatorTest.cs new file mode 100644 index 0000000..bca7778 --- /dev/null +++ b/test/ChangeDB.Agent.Sqlite.IntegrationTest/SqliteSqlExpressionTranslatorTest.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Linq; +using System.Threading.Tasks; +using ChangeDB.Descriptors; +using ChangeDB.Migration; +using FluentAssertions; +using Xunit; + +namespace ChangeDB.Agent.Sqlite +{ + public class SqliteSqlExpressionTranslatorTest : BaseTest + { + [Theory] + [ClassData(typeof(MapToCommonSqlExpression))] + public async Task ShouldMapToCommonSqlExpression(string sqlExpression, string storeType, SqlExpressionDescriptor sqlExpressionDescriptor) + { + var metadataMigrator = SqliteMetadataMigrator.Default; + var defaultValue = string.IsNullOrEmpty(sqlExpression) ? string.Empty : $"default {sqlExpression}"; + using var database = CreateDatabase(false, $"create table t1(c1 {storeType} {defaultValue})"); + var context = new AgentContext + { + ConnectionString = database.ConnectionString, + Connection = database.Connection, + }; + var databaseDesc = await metadataMigrator.GetDatabaseDescriptor(context); + var tableDesc = databaseDesc.Tables.Single(); + var columnDesc = tableDesc.Columns.Single(); + columnDesc.DefaultValue.Should().BeEquivalentTo(sqlExpressionDescriptor); + } + [Theory] + [ClassData(typeof(MapFromCommonSqlExpression))] + public async Task ShouldMapFromCommonSqlExpression(SqlExpressionDescriptor sourceSqlExpression, DataTypeDescriptor dataType, string sqlExpression) + { + var metadataMigrator = SqliteMetadataMigrator.Default; + + using var database = CreateDatabase(false); + var context = new AgentContext + { + ConnectionString = database.ConnectionString, + Connection = database.Connection, + }; + var databaseDesc = new DatabaseDescriptor + { + Tables = new List + { + new TableDescriptor + { + Name = "t1", + Columns = new List + { + new ColumnDescriptor + { + Name = "c1", + DataType = dataType, + DefaultValue = sourceSqlExpression, + } + } + } + } + }; + await metadataMigrator.MigrateAllMetaData(databaseDesc, context); + + var databaseDescFromDB = await metadataMigrator.GetDatabaseDescriptor(context); + var tableDesc = databaseDescFromDB.Tables.Single(); + var columnDesc = tableDesc.Columns.Single(); + columnDesc.GetOriginDefaultValue().Should().Be(sqlExpression); + + } + + class MapFromCommonSqlExpression : List + { + public MapFromCommonSqlExpression() + { + Add(null!, DataTypeDescriptor.Int(), null!); + Add(null!, DataTypeDescriptor.Int(), null!); + Add(SqlExpressionDescriptor.FromConstant(null), DataTypeDescriptor.Int(), null!); + Add(SqlExpressionDescriptor.FromFunction(Function.Now), DataTypeDescriptor.DateTime(6), "CURRENT_TIMESTAMP"); + Add(SqlExpressionDescriptor.FromFunction(Function.Uuid), DataTypeDescriptor.Uuid(), "randomblob(16)"); + Add(SqlExpressionDescriptor.FromConstant(123), DataTypeDescriptor.Int(), "123"); + Add(SqlExpressionDescriptor.FromConstant(123L), DataTypeDescriptor.BigInt(), "123"); + Add(SqlExpressionDescriptor.FromConstant(true), DataTypeDescriptor.Boolean(), "1"); + Add(SqlExpressionDescriptor.FromConstant(false), DataTypeDescriptor.Boolean(), "0"); + Add(SqlExpressionDescriptor.FromConstant(123.45), DataTypeDescriptor.Double(), "123.45"); + Add(SqlExpressionDescriptor.FromConstant(123.45M), DataTypeDescriptor.Decimal(10, 2), "123.45"); + Add(SqlExpressionDescriptor.FromConstant(""), DataTypeDescriptor.Varchar(10), "''"); + Add(SqlExpressionDescriptor.FromConstant("'"), DataTypeDescriptor.Varchar(10), "''''"); + Add(SqlExpressionDescriptor.FromConstant("''"), DataTypeDescriptor.Varchar(10), "''''''"); + Add(SqlExpressionDescriptor.FromConstant("abc"), DataTypeDescriptor.Varchar(10), "'abc'"); + Add(SqlExpressionDescriptor.FromConstant(""), DataTypeDescriptor.Char(10), "''"); + Add(SqlExpressionDescriptor.FromConstant("'"), DataTypeDescriptor.Char(10), "''''"); + Add(SqlExpressionDescriptor.FromConstant("''"), DataTypeDescriptor.Char(10), "''''''"); + Add(SqlExpressionDescriptor.FromConstant("abc"), DataTypeDescriptor.Char(10), "'abc'"); + Add(SqlExpressionDescriptor.FromConstant(Guid.Empty), DataTypeDescriptor.Uuid(), "'00000000-0000-0000-0000-000000000000'"); + Add(SqlExpressionDescriptor.FromConstant(new byte[] { 1, 15 }), DataTypeDescriptor.Varbinary(10), "x'010F'"); + Add(SqlExpressionDescriptor.FromConstant(new byte[] { 1, 15 }), DataTypeDescriptor.Binary(10), "x'010F'"); + Add(SqlExpressionDescriptor.FromConstant(new DateTime(2021, 11, 24)), DataTypeDescriptor.DateTime(6), "'2021-11-24'"); + Add(SqlExpressionDescriptor.FromConstant(new DateTime(2021, 11, 24, 18, 54, 1)), DataTypeDescriptor.DateTime(6), "'2021-11-24 18:54:01'"); + Add(SqlExpressionDescriptor.FromConstant(new DateTime(2021, 11, 24, 18, 54, 1, 123)), DataTypeDescriptor.DateTime(6), "'2021-11-24 18:54:01.123'"); + Add(SqlExpressionDescriptor.FromConstant(DateTimeOffset.Parse("2021-11-24")), DataTypeDescriptor.DateTimeOffset(6), $"'{DateTimeOffset.Parse("2021-11-24"):yyyy-MM-dd HH:mm:ss zzz}'"); + Add(SqlExpressionDescriptor.FromConstant(DateTimeOffset.Parse("2021-11-24 18:54:01")), DataTypeDescriptor.DateTimeOffset(6), $"'{DateTimeOffset.Parse("2021-11-24 18:54:01"):yyyy-MM-dd HH:mm:ss zzz}'"); + Add(SqlExpressionDescriptor.FromConstant(DateTimeOffset.Parse("2021-11-24 18:54:01.123")), DataTypeDescriptor.DateTimeOffset(6), $"'{DateTimeOffset.Parse("2021-11-24 18:54:01.123"):yyyy-MM-dd HH:mm:ss.fff zzz}'"); + Add(SqlExpressionDescriptor.FromConstant(DateTimeOffset.Parse("2021-11-24 18:54:01 +08")), DataTypeDescriptor.DateTimeOffset(6), $"'{DateTimeOffset.Parse("2021-11-24 18:54:01 +08:00"):yyyy-MM-dd HH:mm:ss zzz}'"); + } + + private void Add(SqlExpressionDescriptor descriptor, DataTypeDescriptor dataType, string targetSqlExpression) + { + Add(new object[] { descriptor, dataType, targetSqlExpression == null ? null! : targetSqlExpression }); + } + } + + class MapToCommonSqlExpression : List + { + public MapToCommonSqlExpression() + { + Add(null!, "int", null!); + Add("null", "int", null!); + Add("", "int", null!); + Add("current_date", "datetime", new SqlExpressionDescriptor() { Function = Function.Now }); + Add("current_time", "datetime", new SqlExpressionDescriptor() { Function = Function.Now }); + Add("current_timestamp", "datetime", new SqlExpressionDescriptor() { Function = Function.Now }); + Add("(randomblob(16))", "uuid", new SqlExpressionDescriptor() { Function = Function.Uuid }); + Add("0", "bit", new SqlExpressionDescriptor() { Constant = false }); + Add("1", "bit", new SqlExpressionDescriptor() { Constant = true }); + Add("123", "int", new SqlExpressionDescriptor() { Constant = 123 }); + Add("(123)", "int", new SqlExpressionDescriptor() { Constant = 123 }); + Add("(123)", "nvarchar(10)", new SqlExpressionDescriptor() { Constant = "123" }); + Add("(123)", "bigint", new SqlExpressionDescriptor() { Constant = 123L }); + Add("(123.45)", "decimal(10,2)", new SqlExpressionDescriptor() { Constant = 123.45m }); + Add("(123.45)", "float", new SqlExpressionDescriptor() { Constant = 123.45f }); + Add("null", "nvarchar(10)", null!); + Add("null", "bigint", null!); + Add("('123')", "int", new SqlExpressionDescriptor() { Constant = 123 }); + Add("('123')", "nvarchar(10)", new SqlExpressionDescriptor() { Constant = "123" }); + Add("('')", "nvarchar(10)", new SqlExpressionDescriptor() { Constant = "" }); + Add("('''')", "nvarchar(10)", new SqlExpressionDescriptor() { Constant = "'" }); + Add("('''''')", "nvarchar(10)", new SqlExpressionDescriptor() { Constant = "''" }); + + Add("('123')", "nchar(10)", new SqlExpressionDescriptor() { Constant = "123" }); + Add("('')", "nchar(10)", new SqlExpressionDescriptor() { Constant = "" }); + Add("('''')", "nchar(10)", new SqlExpressionDescriptor() { Constant = "'" }); + Add("('''''')", "nchar(10)", new SqlExpressionDescriptor() { Constant = "''" }); + + Add("('123')", "varchar(10)", new SqlExpressionDescriptor() { Constant = "123" }); + Add("('')", "varchar(10)", new SqlExpressionDescriptor() { Constant = "" }); + Add("('''')", "varchar(10)", new SqlExpressionDescriptor() { Constant = "'" }); + Add("('''''')", "varchar(10)", new SqlExpressionDescriptor() { Constant = "''" }); + + Add("('123')", "char(10)", new SqlExpressionDescriptor() { Constant = "123" }); + Add("('')", "char(10)", new SqlExpressionDescriptor() { Constant = "" }); + Add("('''')", "char(10)", new SqlExpressionDescriptor() { Constant = "'" }); + Add("('''''')", "char(10)", new SqlExpressionDescriptor() { Constant = "''" }); + + Add("('00000000-0000-0000-0000-000000000000')", "uniqueidentifier", new SqlExpressionDescriptor() { Constant = Guid.Empty }); + Add("x'1122'", "varbinary(5)", new SqlExpressionDescriptor() { Constant = new byte[] { 0x11, 0x22 } }); + Add("x'1122'", "binary(5)", new SqlExpressionDescriptor() { Constant = new byte[] { 0x11, 0x22 } }); + Add("'2021-11-24 18:54:01'", "datetime2", new SqlExpressionDescriptor() { Constant = new DateTime(2021, 11, 24, 18, 54, 1) }); + Add("'2021-11-24 18:54:01 +08:00'", "datetimeoffset", new SqlExpressionDescriptor() { Constant = DateTimeOffset.Parse("2021-11-24 18:54:01 +08:00") }); + } + private void Add(string sqlExpression, string storeType, SqlExpressionDescriptor descriptor) + { + this.Add(new Object[] { sqlExpression, storeType, descriptor }); + } + } + } +} diff --git a/test/ChangeDB.Agent.Sqlite.UnitTest/ChangeDB.Agent.Sqlite.UnitTest.csproj b/test/ChangeDB.Agent.Sqlite.UnitTest/ChangeDB.Agent.Sqlite.UnitTest.csproj new file mode 100644 index 0000000..620f838 --- /dev/null +++ b/test/ChangeDB.Agent.Sqlite.UnitTest/ChangeDB.Agent.Sqlite.UnitTest.csproj @@ -0,0 +1,12 @@ + + + + enable + ChangeDB.Agent.Sqlite + + + + + + + diff --git a/test/ChangeDB.Agent.Sqlite.UnitTest/SqliteConnectionProviderTest.cs b/test/ChangeDB.Agent.Sqlite.UnitTest/SqliteConnectionProviderTest.cs new file mode 100644 index 0000000..a9567c0 --- /dev/null +++ b/test/ChangeDB.Agent.Sqlite.UnitTest/SqliteConnectionProviderTest.cs @@ -0,0 +1,17 @@ +using ChangeDB.Agent.Sqlite; +using FluentAssertions; +using Microsoft.Data.Sqlite; +using Xunit; + +namespace ChangeDB.Agent.SqlServer +{ + public class SqlServerConnectionProviderTest + { + [Fact] + public void ShouldGetSqlCeConnection() + { + var provider = new SqliteConnectionProvider(); + provider.CreateConnection("Data Source=sqlite.db").Should().BeOfType(); + } + } +} diff --git a/testdb/TestDB.Sqlite/SqliteInstance.cs b/testdb/TestDB.Sqlite/SqliteInstance.cs new file mode 100644 index 0000000..dafadb7 --- /dev/null +++ b/testdb/TestDB.Sqlite/SqliteInstance.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TestDB.Sqlite +{ + public class SqliteInstance : IDatabaseInstance + { + public SqliteInstance() : this("TESTDB_SQLITE") + { + + } + public SqliteInstance(string envName) + { + this.ConnectionTemplate = Environment.GetEnvironmentVariable(envName, EnvironmentVariableTarget.Process) + ?? Environment.GetEnvironmentVariable(envName, EnvironmentVariableTarget.User) + ?? Environment.GetEnvironmentVariable(envName, EnvironmentVariableTarget.Machine) + ?? $"Data Source=sqlite.db;"; + } + + + + public string ConnectionTemplate { get; } + + private readonly IDisposable dockerContainer; + + public void Dispose() + { + if (dockerContainer != null) + { + dockerContainer.Dispose(); + } + } + + public async ValueTask DisposeAsync() + { + await Task.Run(Dispose); + } + } +} diff --git a/testdb/TestDB.Sqlite/SqliteProvider.cs b/testdb/TestDB.Sqlite/SqliteProvider.cs new file mode 100644 index 0000000..c72693e --- /dev/null +++ b/testdb/TestDB.Sqlite/SqliteProvider.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Data.Sqlite; + +namespace TestDB.Sqlite +{ + public class SqliteProvider : BaseServiceProvider + { + private static string IdentityName(string name) + { + return $"\"{name}\""; + } + private static string IdentityName(string schema, string name) + { + return string.IsNullOrEmpty(schema) ? IdentityName(name) : $"{IdentityName(schema)}.{IdentityName(name)}"; + } + public override bool SupportFastClone => true; + + public override string ChangeDatabase(string connectionString, string databaseName) + { + return new SqliteConnectionStringBuilder(connectionString) + { + DataSource = $"{databaseName}.db" + }.ConnectionString; + } + + public override void CleanDatabase(string connectionString) + { + //using var connection = CreateConnection(connectionString); + //DropAllForeignConstraints(); + //DropAllSchemas(); + //void DropAllSchemas() + //{ + // var allSchemas = connection.ExecuteReaderAsList("select schema_name from information_schema.schemata where schema_owner = 'dbo'"); + // allSchemas.ToList().ForEach(p => DropSchemaIfExists(p, p != "dbo")); + //} + + //void DropSchemaIfExists(string schema, bool dropSchema = true) + //{ + // var allViews = connection.ExecuteReaderAsList($"SELECT table_name from INFORMATION_SCHEMA.TABLES t WHERE t.TABLE_SCHEMA = '{schema}' AND t.TABLE_TYPE ='VIEW'"); + // allViews.ForEach(p => DropViewIfExists(schema, p)); + + // var allTables = connection.ExecuteReaderAsList($"SELECT table_name from INFORMATION_SCHEMA.TABLES t WHERE t.TABLE_SCHEMA = '{schema}' AND t.TABLE_TYPE ='BASE TABLE'"); + // allTables.ForEach(p => DropTableIfExists(schema, p)); + // if (dropSchema) + // { + // connection.ExecuteNonQuery($"drop schema if exists {IdentityName(schema)}"); + // } + //} + //void DropTableIfExists(string schema, string table) + //{ + // connection.ExecuteNonQuery($"drop table if exists {IdentityName(schema, table)}"); + //} + //void DropViewIfExists(string schema, string view) + //{ + // connection.ExecuteNonQuery($"drop view if exists {IdentityName(schema, view)}"); + //} + //void DropAllForeignConstraints() + //{ + // var allForeignConstraints = connection.ExecuteReaderAsList($"SELECT TABLE_NAME ,CONSTRAINT_NAME,TABLE_SCHEMA from INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc where tc.CONSTRAINT_TYPE ='FOREIGN KEY'"); + // allForeignConstraints.ForEach(p => connection.ExecuteNonQuery($"ALTER TABLE {IdentityName(p.Item3, p.Item1)} drop constraint {IdentityName(p.Item2)};")); + //} + } + + public override void CloneDatabase(string connectionString, string newDatabaseName) + { + using var newConnection = CreateNoDatabaseConnection(connectionString); + newConnection.ExecuteNonQuery( + $"DBCC CLONEDATABASE ({IdentityName(GetDatabaseName(connectionString))}, {IdentityName(newDatabaseName)})"); + newConnection.ExecuteNonQuery($"ALTER DATABASE {IdentityName(GetDatabaseName(connectionString))} SET READ_WRITE WITH NO_WAIT"); + + } + + public override DbConnection CreateConnection(string connectionString) + { + return new SqliteConnection(connectionString); + } + + public override void CreateDatabase(string connectionString) + { + Console.WriteLine($"create database '{connectionString}'"); + } + + public override void DropTargetDatabaseIfExists(string connectionString) + { + var builder = new SqliteConnectionStringBuilder(connectionString); + var fileName = builder.DataSource; + lock (fileName) + { + if (File.Exists(fileName)) + { + SqliteConnection.ClearAllPools(); + File.Delete(fileName); + } + } + } + + public override string MakeReadOnly(string connectionString) + { + using var newConnection = CreateNoDatabaseConnection(connectionString); + newConnection.ExecuteNonQuery($"ALTER DATABASE {IdentityName(GetDatabaseName(connectionString))} SET READ_ONLY WITH NO_WAIT"); + return connectionString; + } + + protected override bool IsSplitLine(string line) + { + return "go".Equals(line?.Trim(), StringComparison.InvariantCultureIgnoreCase); + } + + private string GetDatabaseName(string connectionString) + { + return new SqliteConnectionStringBuilder(connectionString).DataSource; + } + private DbConnection CreateNoDatabaseConnection(string connection) + { + return new SqliteConnection(ChangeDatabase(connection, string.Empty)); + } + } + +} diff --git a/testdb/TestDB.Sqlite/TestDB.Sqlite.csproj b/testdb/TestDB.Sqlite/TestDB.Sqlite.csproj new file mode 100644 index 0000000..4ece4eb --- /dev/null +++ b/testdb/TestDB.Sqlite/TestDB.Sqlite.csproj @@ -0,0 +1,8 @@ + + + + + + + +