diff --git a/source/Nevermore.IntegrationTests/Advanced/CompositeIdentityFixture.cs b/source/Nevermore.IntegrationTests/Advanced/CompositeIdentityFixture.cs new file mode 100644 index 00000000..08d86c40 --- /dev/null +++ b/source/Nevermore.IntegrationTests/Advanced/CompositeIdentityFixture.cs @@ -0,0 +1,111 @@ +using System; +using System.Data; +using System.Data.Common; +using FluentAssertions; +using Nevermore.IntegrationTests.SetUp; +using Nevermore.Mapping; +using NUnit.Framework; +using Microsoft.Data.SqlClient.Server; +using Nevermore.Advanced.TypeHandlers; + +namespace Nevermore.IntegrationTests.Advanced +{ + [TestFixture] + public class CompositeIdentityFixture : FixtureWithRelationalStore + { + public override void OneTimeSetUp() + { + base.OneTimeSetUp(); + + ExecuteSql("create table TestSchema.DatabaseModel (Id INT IDENTITY, Name varchar(12))"); + Mappings.Register(new DatabaseModelMap()); + Configuration.TypeHandlers.Register(new CompositeTypeHandler()); + } + + class Composite + { + public int Value { get; } + + public Composite(int value) + { + Value = value; + } + } + + class DatabaseModel + { + public Composite Id { get; private set; } + public string Name { get; private set; } + + DatabaseModel() + { + } + + public DatabaseModel(string name) + { + Name = name; + } + } + + class DatabaseModelMap : DocumentMap + { + public DatabaseModelMap() + { + Id(m => m.Id) + .Identity() + .KeyHandler(new CompositePrimaryKeyHandler()); + Column(m => m.Name); + JsonStorageFormat = JsonStorageFormat.NoJson; + } + } + class CompositeTypeHandler : ITypeHandler + { + public bool CanConvert(Type objectType) + { + return objectType == typeof(Composite); + } + + public object ReadDatabase(DbDataReader reader, int columnIndex) + { + if (reader.IsDBNull(columnIndex)) + return default(Composite); + var value = reader.GetInt32(columnIndex); + return new Composite(value); + } + + public void WriteDatabase(DbParameter parameter, object value) + { + parameter.Value = ((Composite) value)?.Value; + } + } + class CompositePrimaryKeyHandler : PrimaryKeyHandler + { + public override SqlMetaData GetSqlMetaData(string name) => new SqlMetaData(name, SqlDbType.Int); + + public override object GetNextKey(IKeyAllocator keyAllocator, string tableName) + { + return keyAllocator.NextId(tableName); + } + } + + [Test] + public void ShouldRoundTrip() + { + var m = new DatabaseModel("Test"); + using (var writeTransaction = Store.BeginWriteTransaction()) + { + writeTransaction.Insert(m); + writeTransaction.Commit(); + } + + using (var readTransaction = Store.BeginReadTransaction()) + { + readTransaction.Load(m.Id.Value) + .Should() + .NotBeNull(); + } + } + } + + +} \ No newline at end of file diff --git a/source/Nevermore/Advanced/ReadTransaction.cs b/source/Nevermore/Advanced/ReadTransaction.cs index 477cd8fc..8ce30cfd 100644 --- a/source/Nevermore/Advanced/ReadTransaction.cs +++ b/source/Nevermore/Advanced/ReadTransaction.cs @@ -598,8 +598,8 @@ PreparedCommand PrepareLoad(TKey id) if (mapping.IdColumn is null) throw new InvalidOperationException($"Cannot load {mapping.Type.Name} by Id, as no Id column has been mapped."); - if (mapping.IdColumn.Type != typeof(TKey)) - throw new ArgumentException($"Provided Id of type '{id?.GetType().FullName}' does not match configured type of '{mapping.IdColumn?.Type.FullName}'."); + // if (mapping.IdColumn.Type != typeof(TKey)) + // throw new ArgumentException($"Provided Id of type '{id?.GetType().FullName}' does not match configured type of '{mapping.IdColumn?.Type.FullName}'."); var columnNames = GetColumnNames(mapping.SchemaName, mapping.TableName); var tableName = mapping.TableName; diff --git a/source/Nevermore/Advanced/WriteTransaction.cs b/source/Nevermore/Advanced/WriteTransaction.cs index d03f0407..1cad0dc8 100644 --- a/source/Nevermore/Advanced/WriteTransaction.cs +++ b/source/Nevermore/Advanced/WriteTransaction.cs @@ -291,7 +291,7 @@ DataModificationOutput[] ExecuteDataModification(PreparedCommand command) //The results need to be read eagerly so errors are raised while code is still executing within CommandExecutor error handling logic return ReadResults(command, reader => DataModificationOutput.Read(reader, command.Mapping, - command.Operation == RetriableOperation.Insert)); + command.Operation == RetriableOperation.Insert, configuration)); } DataModificationOutput ExecuteSingleDataModification(PreparedCommand command) @@ -365,7 +365,7 @@ class DataModificationOutput public byte[] RowVersion { get; private set; } public object Id { get; private set; } - public static DataModificationOutput Read(DbDataReader reader, DocumentMap map, bool isInsert) + public static DataModificationOutput Read(DbDataReader reader, DocumentMap map, bool isInsert, IRelationalStoreConfiguration configuration) { var output = new DataModificationOutput(); @@ -374,7 +374,12 @@ public static DataModificationOutput Read(DbDataReader reader, DocumentMap map, reader.GetFieldValue(map.RowVersionColumn!.ColumnName); if (map.IsIdentityId && isInsert) - output.Id = reader.GetFieldValue(map.IdColumn!.ColumnName); + { + var typeHandler = configuration.TypeHandlers.Resolve(map.IdColumn.Type); + output.Id = typeHandler == null + ? reader.GetFieldValue(map.IdColumn!.ColumnName) + : typeHandler.ReadDatabase(reader, 0); + } return output; } diff --git a/source/Nevermore/Mapping/IdColumnMapping.cs b/source/Nevermore/Mapping/IdColumnMapping.cs index a396efcd..3a0d8a28 100644 --- a/source/Nevermore/Mapping/IdColumnMapping.cs +++ b/source/Nevermore/Mapping/IdColumnMapping.cs @@ -1,6 +1,7 @@ #nullable enable using System; using System.Collections.Generic; +using System.Data; using System.Reflection; namespace Nevermore.Mapping @@ -19,19 +20,25 @@ internal IdColumnMapping(IdColumnMappingBuilder idColumn, IPrimaryKeyHandler pri public bool IsIdentity { get; } public IPrimaryKeyHandler PrimaryKeyHandler { get; } - } - - public class IdColumnMappingBuilder : ColumnMapping, IIdColumnMappingBuilder - { - static readonly HashSet ValidIdentityTypes = new HashSet + + static readonly HashSet ValidIdentityTypes = new () { - typeof(short), - typeof(int), - typeof(long) + SqlDbType.SmallInt, + SqlDbType.Int, + SqlDbType.BigInt }; - bool hasCustomPropertyHandler; + void ValidateForIdentityUse() + { + if (!IsIdentity) + return; + if (!ValidIdentityTypes.Contains(PrimaryKeyHandler.GetSqlMetaData(ColumnName).SqlDbType)) + throw new InvalidOperationException($"The type {Type.Name} is not supported for Identity columns. Identity columns must be one of 'short', 'int' or 'long'."); + } + } + public class IdColumnMappingBuilder : ColumnMapping, IIdColumnMappingBuilder + { internal IdColumnMappingBuilder(string columnName, Type type, IPropertyHandler handler, PropertyInfo property) : base(columnName, type, handler, property) { } @@ -43,8 +50,6 @@ internal IdColumnMappingBuilder(string columnName, Type type, IPropertyHandler h /// public IIdColumnMappingBuilder Identity() { - ValidateForIdentityUse(); - IsIdentity = true; Direction = ColumnDirection.FromDatabase; @@ -57,21 +62,11 @@ public IIdColumnMappingBuilder KeyHandler(IPrimaryKeyHandler primaryKeyHandler) return this; } - void ValidateForIdentityUse() - { - if (!ValidIdentityTypes.Contains(Type)) - throw new InvalidOperationException($"The type {Type.Name} is not supported for Identity columns. Identity columns must be one of 'short', 'int' or 'long'."); - - if (hasCustomPropertyHandler) - throw new InvalidOperationException("Unable to configure an Identity Id column with a custom PropertyHandler"); - } - protected override void SetCustomPropertyHandler(IPropertyHandler propertyHandler) { if (Direction == ColumnDirection.FromDatabase) throw new InvalidOperationException("Unable to configure an Identity Id column with a custom PropertyHandler"); - hasCustomPropertyHandler = true; base.SetCustomPropertyHandler(propertyHandler); } diff --git a/source/Nevermore/Util/DataModificationQueryBuilder.cs b/source/Nevermore/Util/DataModificationQueryBuilder.cs index 24140d3f..2f8e548f 100644 --- a/source/Nevermore/Util/DataModificationQueryBuilder.cs +++ b/source/Nevermore/Util/DataModificationQueryBuilder.cs @@ -231,7 +231,7 @@ void AppendInsertStatement(StringBuilder sb, DocumentMap mapping, string tableNa outputColumns.Add(mapping.RowVersionColumn.ColumnName, "binary(8)"); if (mapping.IsIdentityId) - outputColumns.Add(mapping.IdColumn.ColumnName, mapping.IdColumn.Type.GetIdentityIdTypeName()); + outputColumns.Add(mapping.IdColumn.ColumnName, mapping.IdColumn.PrimaryKeyHandler.GetSqlMetaData(mapping.IdColumn.ColumnName).SqlDbType.ToString()); outputStatement = $"OUTPUT {string.Join(",", outputColumns.Select(kvp => $"inserted.[{kvp.Key}]"))} INTO @InsertedRows"; outputVariable = $"DECLARE @InsertedRows TABLE ({string.Join(", ", outputColumns.Select(kvp => $"[{kvp.Key}] {kvp.Value}"))})";