diff --git a/msssql-cdc.sln b/msssql-cdc.sln index 4a52020..6e29f69 100644 --- a/msssql-cdc.sln +++ b/msssql-cdc.sln @@ -11,6 +11,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{5D EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example", "examples\Example\Example.csproj", "{7DC5DA8F-274F-4586-97F4-490E0F12AE09}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{E9EECC5D-1729-4F68-B525-BD7CEE63BC05}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MsSqlCdc.Tests", "test\MsSqlCdc.Tests\MsSqlCdc.Tests.csproj", "{B9FA94EC-6E90-43CA-ACB6-D5BE12AB90AF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -28,9 +32,14 @@ Global {7DC5DA8F-274F-4586-97F4-490E0F12AE09}.Debug|Any CPU.Build.0 = Debug|Any CPU {7DC5DA8F-274F-4586-97F4-490E0F12AE09}.Release|Any CPU.ActiveCfg = Release|Any CPU {7DC5DA8F-274F-4586-97F4-490E0F12AE09}.Release|Any CPU.Build.0 = Release|Any CPU + {B9FA94EC-6E90-43CA-ACB6-D5BE12AB90AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B9FA94EC-6E90-43CA-ACB6-D5BE12AB90AF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B9FA94EC-6E90-43CA-ACB6-D5BE12AB90AF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B9FA94EC-6E90-43CA-ACB6-D5BE12AB90AF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {5349AA55-B231-44AA-B411-97590DB9BA49} = {69F94C69-D4A8-4347-BCF6-A28CA7A2EBA4} {7DC5DA8F-274F-4586-97F4-490E0F12AE09} = {5DFEC30A-0E3B-42CA-A0EA-6743821DE000} + {B9FA94EC-6E90-43CA-ACB6-D5BE12AB90AF} = {E9EECC5D-1729-4F68-B525-BD7CEE63BC05} EndGlobalSection EndGlobal diff --git a/src/MsSqlCdc/ChangeData.cs b/src/MsSqlCdc/ChangeData.cs index 839dea8..dd24d93 100644 --- a/src/MsSqlCdc/ChangeData.cs +++ b/src/MsSqlCdc/ChangeData.cs @@ -2,7 +2,7 @@ namespace MsSqlCdc; -public enum Operation : ushort +public enum Operation { Delete = 1, Insert = 2, diff --git a/src/MsSqlCdc/DataConvert.cs b/src/MsSqlCdc/DataConvert.cs index addfe0c..7463b1c 100644 --- a/src/MsSqlCdc/DataConvert.cs +++ b/src/MsSqlCdc/DataConvert.cs @@ -11,17 +11,21 @@ public static class DataConvert /// /// Converts a a colection of columns represented as Tuple to ChangeData representation. /// - /// List of tuples with Item1 being the name column and Item2 being the column value + /// List of tuples with Item1 being the name column and Item2 being the column value /// The tablename of the column. /// Returns the CDC column as a ChangeData record. - public static ChangeData ConvertCdcColumn(List> column, string captureInstance) + /// + public static ChangeData ConvertCdcColumn(List> columnFields, string captureInstance) { - var startLsn = ConvertBinaryLsn((byte[])column[0].Item2); - var seqVal = ConvertBinaryLsn((byte[])column[1].Item2); - var operation = ConvertIntOperation((int)column[2].Item2); - var updateMask = Encoding.UTF8.GetString((byte[])column[3].Item2); + if (columnFields.Count < 3) + throw new Exception($"Count of column fields should be 4 or greater, instead got '{columnFields.Count}'."); - var body = column.Skip(4) + var startLsn = ConvertBinaryLsn((byte[])columnFields[0].Item2); + var seqVal = ConvertBinaryLsn((byte[])columnFields[1].Item2); + var operation = ConvertIntOperation((int)columnFields[2].Item2); + var updateMask = Encoding.UTF8.GetString((byte[])columnFields[3].Item2); + + var body = columnFields.Skip(4) .Aggregate(new ExpandoObject() as IDictionary, (acc, x) => { acc[x.Item1] = x.Item2; return acc; }) as dynamic; diff --git a/test/MsSqlCdc.Tests/DataConvertTest.cs b/test/MsSqlCdc.Tests/DataConvertTest.cs new file mode 100644 index 0000000..c47da39 --- /dev/null +++ b/test/MsSqlCdc.Tests/DataConvertTest.cs @@ -0,0 +1,105 @@ +using Xunit; +using FluentAssertions; +using static FluentAssertions.FluentActions; +using System; +using System.Collections.Generic; +using System.Text; +using System.Linq; +using FluentAssertions.Execution; + +namespace MsSqlCdc.Tests; + +public class DataConverTest +{ + [Theory] + [InlineData(25000L, 25002L, Operation.AfterUpdate, "ABB", 10, "Rune", 20000.00)] + [InlineData(25L, 25202L, Operation.BeforeUpdate, "DSDFS", 2, "Simon", 0.00)] + [InlineData(250000000L, 250021L, Operation.Delete, "DFS", 3, "Foo", 1000000000.00)] + [InlineData(0L, 2L, Operation.Insert, "DFS", 3, "John", 1000000000.00)] + public void ConvertCdcColumn_ShouldReturnChangeData_OnValidInput( + long startLsn, + long seqVal, + Operation operation, + string updateMask, + int id, + string name, + double salary) + { + var captureInstance = "dbo_Employee"; + var columnFields = new List> + { + new Tuple("__$start_lsn", BitConverter.GetBytes(startLsn).Reverse().ToArray()), + new Tuple("__$seqval", BitConverter.GetBytes(seqVal).Reverse().ToArray()), + new Tuple("__$operation", (int)operation), + new Tuple("__$update_mask", Encoding.ASCII.GetBytes(updateMask)), + new Tuple("Id", id), + new Tuple("Name", name), + new Tuple("Salary", salary), + }; + + var changeData = new ChangeData( + startLsn, + seqVal, + operation, + updateMask, + "dbo_Employee", + new + { + Id = id, + Name = name, + Salary = salary, + } + ); + + var result = DataConvert.ConvertCdcColumn(columnFields, captureInstance); + + using (var scope = new AssertionScope()) + { + Assert.True(result.Body.Id == changeData.Body.Id); + Assert.True(result.Body.Name == changeData.Body.Name); + Assert.True(result.Body.Salary == changeData.Body.Salary); + result.StartLineSequenceNumber.Should().Be(changeData.StartLineSequenceNumber); + result.SequenceValue.Should().Be(changeData.SequenceValue); + result.Operation.Should().Be(changeData.Operation); + result.UpdateMask.Should().Be(changeData.UpdateMask); + } + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + public void ConvertCdcColumn_ShouldThrowException_OnColumnFieldsBeingLessThanFour(int columnFieldsCount) + { + var captureInstance = "dbo_Employee"; + + var columnFields = Enumerable.Range(0, columnFieldsCount) + .Select(x => new Tuple(x.ToString(), x)).ToList(); + + Invoking(() => DataConvert.ConvertCdcColumn(columnFields, captureInstance)).Should().Throw(); + } + + [Theory] + [InlineData(1, Operation.Delete)] + [InlineData(2, Operation.Insert)] + [InlineData(3, Operation.BeforeUpdate)] + [InlineData(4, Operation.AfterUpdate)] + public void ConvertIntOperation_ShouldReturnCorrectEnumConvertion(int input, Operation expected) + { + var operation = DataConvert.ConvertIntOperation(input); + operation.Should().Be(expected); + } + + [Theory] + [InlineData(-1)] + [InlineData(-10)] + [InlineData(0)] + [InlineData(100)] + [InlineData(int.MinValue)] + [InlineData(int.MaxValue)] + public void ConvertIntOperation_ShouldThrowException_OnInvalidIntRepresentation(int input) + { + Invoking(() => DataConvert.ConvertIntOperation(input)).Should().Throw(); + } +} diff --git a/test/MsSqlCdc.Tests/MsSqlCdc.Tests.csproj b/test/MsSqlCdc.Tests/MsSqlCdc.Tests.csproj new file mode 100644 index 0000000..86171f8 --- /dev/null +++ b/test/MsSqlCdc.Tests/MsSqlCdc.Tests.csproj @@ -0,0 +1,28 @@ + + + + net6.0 + enable + + false + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + +