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