Skip to content

Commit 6e8911c

Browse files
committed
DateOnly and TimeOnly support
1 parent 2b0e1f1 commit 6e8911c

16 files changed

+1010
-3
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#if NET6_0_OR_GREATER
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Linq.Expressions;
5+
using NHibernate.Type;
6+
7+
namespace NHibernate.Test.TypesTest
8+
{
9+
public class AbstractTimeOnlyTypeFixture<TType> : GenericTypeFixtureBase<TimeOnly, TType> where TType : IType
10+
{
11+
protected override IReadOnlyList<TimeOnly> TestValues => [new(12, 13, 14), new(23, 59, 59), new(0, 0, 0)];
12+
protected override IEnumerable<Expression<Func<TimeOnly, object>>> PropertiesToTestWithLinq
13+
{
14+
get
15+
{
16+
yield return (TimeOnly x) => x.Hour;
17+
yield return (TimeOnly x) => x.Minute;
18+
yield return (TimeOnly x) => x.Second;
19+
}
20+
}
21+
}
22+
}
23+
#endif
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#if NET6_0_OR_GREATER
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
6+
using NHibernate.Mapping.ByCode;
7+
using NHibernate.Type;
8+
9+
namespace NHibernate.Test.TypesTest
10+
{
11+
public abstract class AbstractTimeOnlyTypeWithScaleFixture<TType> : AbstractTimeOnlyTypeFixture<TType> where TType : IType
12+
{
13+
private readonly bool _setMaxScale;
14+
protected AbstractTimeOnlyTypeWithScaleFixture(bool setMaxScale)
15+
{
16+
_setMaxScale = setMaxScale;
17+
}
18+
19+
/// <summary>
20+
/// The resolution used when setMaxScale is true
21+
/// </summary>
22+
protected virtual long MaxTimestampResolutionInTicks => Dialect.TimestampResolutionInTicks;
23+
24+
/// <summary>
25+
/// Add fractional seconds to the test values when setMaxScale is true
26+
/// </summary>
27+
protected override IReadOnlyList<TimeOnly> TestValues => [.. base.TestValues.Select(x => _setMaxScale ? AdjustTestValueWithFractionalSeconds(x) : x)];
28+
29+
private TimeOnly AdjustTestValueWithFractionalSeconds(TimeOnly value)
30+
{
31+
value = new TimeOnly(value.Hour,value.Minute,value.Second);
32+
var ticks = value.Ticks + MaxTimestampResolutionInTicks;
33+
if (ticks + MaxTimestampResolutionInTicks > TimeOnly.MaxValue.Ticks)
34+
{
35+
ticks = value.Ticks - MaxTimestampResolutionInTicks;
36+
}
37+
return new TimeOnly(ticks);
38+
}
39+
40+
protected override void ConfigurePropertyMapping<TPersistentType>(IPropertyMapper propertyMapper)
41+
{
42+
if (_setMaxScale)
43+
{
44+
propertyMapper.Scale((short) Math.Floor(Math.Log10(TimeSpan.TicksPerSecond / MaxTimestampResolutionInTicks)));
45+
}
46+
}
47+
}
48+
}
49+
#endif
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#if NET6_0_OR_GREATER
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Linq.Expressions;
5+
using NHibernate.Type;
6+
7+
namespace NHibernate.Test.TypesTest
8+
{
9+
public class DateOnlyAsDateTypeFixture : GenericTypeFixtureBase<DateOnly, DateOnlyAsDateType>
10+
{
11+
protected override IReadOnlyList<DateOnly> TestValues =>
12+
[
13+
DateOnly.FromDateTime(Sfi.ConnectionProvider.Driver.MinDate.AddDays(1)),
14+
DateOnly.FromDateTime(DateTime.Now),
15+
DateOnly.MaxValue.AddDays(-1)
16+
];
17+
18+
protected override IList<Expression<Func<DateOnly, object>>> PropertiesToTestWithLinq =>
19+
[
20+
(DateOnly x) => x.Year,
21+
(DateOnly x) => x.Month,
22+
(DateOnly x) => x.Day
23+
];
24+
}
25+
}
26+
#endif
Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Linq.Expressions;
5+
using NHibernate.Cfg;
6+
using NHibernate.Linq;
7+
using NHibernate.Linq.Visitors;
8+
using NHibernate.Mapping.ByCode;
9+
using NHibernate.Type;
10+
using NUnit.Framework;
11+
12+
namespace NHibernate.Test.TypesTest
13+
{
14+
/// <summary>
15+
/// Base class for fixtures testing individual types, created to avoid
16+
/// code duplication in derived classes.
17+
/// </summary>
18+
public abstract class GenericTypeFixtureBase<TProperty, TType> : TestCase where TType : IType
19+
{
20+
21+
protected override string MappingsAssembly
22+
{
23+
get { return "NHibernate.Test"; }
24+
}
25+
26+
protected override string[] Mappings
27+
{
28+
get
29+
{
30+
return new string[] { };
31+
}
32+
}
33+
34+
/// <summary>
35+
/// Creates a set of test values of type <typeparamref name="TProperty"/>.
36+
/// </summary>
37+
/// <returns>The test values</returns>
38+
/// <remarks>If the type is IComparable, make sure that the first value
39+
/// doesn't become invalid when incremented by <see cref="IncrementValue(TProperty)"/></remarks>
40+
protected abstract IReadOnlyList<TProperty> TestValues { get; }
41+
42+
/// <summary>
43+
/// Override in order to adjust the value of a property before comparisons.
44+
/// May be necessary when dealing with expected precision losses
45+
/// </summary>
46+
/// <param name="value">The value to adjust</param>
47+
/// <returns>The adjusted value</returns>
48+
protected virtual TProperty AdjustValue(TProperty value) => value;
49+
50+
/// <summary>
51+
/// Sub properties of <see cref="TType"/> which should be queryable using LINQ
52+
/// </summary>
53+
protected virtual IEnumerable<Expression<Func<TProperty, object>>> PropertiesToTestWithLinq => Enumerable.Empty<Expression<Func<TProperty, object>>>();
54+
55+
/// <summary>
56+
/// Methods of <see cref="TType"/> which should be queryable using LINQ
57+
/// </summary>
58+
protected virtual IEnumerable<Expression<Func<TProperty, object>>> MethodsToTestWithLinq => Enumerable.Empty<Expression<Func<TProperty, object>>>();
59+
60+
[Test]
61+
public virtual void CanPersist()
62+
{
63+
var testValues = GetAllTestValues();
64+
65+
Dictionary<Guid, TProperty> expectedValues = [];
66+
using (var session = OpenSession())
67+
using (var trans = session.BeginTransaction())
68+
{
69+
foreach (var testValue in testValues)
70+
{
71+
var entity = new TestEntity { TestProperty = testValue };
72+
session.Save(entity);
73+
expectedValues[entity.Id] = testValue;
74+
}
75+
trans.Commit();
76+
}
77+
78+
using (var session = OpenSession())
79+
using (var trans = session.BeginTransaction())
80+
{
81+
foreach (var expectedValue in expectedValues)
82+
{
83+
var entity = session.Get<TestEntity>(expectedValue.Key);
84+
Assert.That(entity, Is.Not.Null);
85+
Assert.That(AdjustValue(entity.TestProperty), Is.EqualTo(AdjustValue(expectedValue.Value)));
86+
}
87+
}
88+
}
89+
90+
[Test]
91+
public void CanQuery()
92+
{
93+
var testValue = GetFirstTestValue();
94+
Guid id;
95+
using (var session = OpenSession())
96+
using (var trans = session.BeginTransaction())
97+
{
98+
var entity = new TestEntity { TestProperty = testValue };
99+
session.Save(entity);
100+
id = entity.Id;
101+
102+
trans.Commit();
103+
}
104+
105+
using (var session = OpenSession())
106+
using (var trans = session.BeginTransaction())
107+
{
108+
var param = Expression.Parameter(typeof(TestEntity));
109+
var prop = Expression.Property(param, nameof(TestEntity.TestProperty));
110+
var value = Expression.Constant(testValue);
111+
var where = Expression.Lambda<Func<TestEntity, bool>>(Expression.Equal(prop, value), param);
112+
var entity = session.Query<TestEntity>().Single(where);
113+
Assert.That(entity, Is.Not.Null);
114+
Assert.That(entity.Id, Is.EqualTo(id));
115+
}
116+
}
117+
118+
private TProperty GetFirstTestValue()
119+
{
120+
var testValues = TestValues;
121+
if (testValues.Count == 0)
122+
{
123+
Assert.Ignore("No test values provided");
124+
}
125+
return testValues[0];
126+
}
127+
128+
private IReadOnlyList<TProperty> GetAllTestValues()
129+
{
130+
var testValues = TestValues;
131+
if (TestValues.Count == 0)
132+
{
133+
Assert.Ignore("No test values provided");
134+
}
135+
return testValues;
136+
}
137+
138+
[Test]
139+
public virtual void CanCompare()
140+
{
141+
if (!typeof(IComparable).IsAssignableFrom(typeof(TProperty)))
142+
{
143+
Assert.Ignore("Not IComparable");
144+
}
145+
var testValues = GetAllTestValues();
146+
147+
if (testValues.Count < 2)
148+
{
149+
Assert.Fail("At least 2 test values required to test comparison");
150+
}
151+
var testValue = testValues[0];
152+
var biggerTestValue = testValues[1];
153+
if (((IComparable) biggerTestValue).CompareTo(testValue) <= 0)
154+
{
155+
Assert.Fail("The second test value must be greater than the first");
156+
return;
157+
}
158+
Guid id;
159+
using (var session = OpenSession())
160+
using (var trans = session.BeginTransaction())
161+
{
162+
var entity = new TestEntity { TestProperty = testValue };
163+
session.Save(entity);
164+
id = entity.Id;
165+
entity = new TestEntity { TestProperty = biggerTestValue };
166+
session.Save(entity);
167+
trans.Commit();
168+
}
169+
170+
using (var session = OpenSession())
171+
using (var trans = session.BeginTransaction())
172+
{
173+
var param = Expression.Parameter(typeof(TestEntity));
174+
var prop = Expression.Property(param, nameof(TestEntity.TestProperty));
175+
var smallerValue = Expression.Constant(testValue, typeof(TProperty));
176+
var biggerValue = Expression.Constant(biggerTestValue, typeof(TProperty));
177+
var smallerWhere = Expression.Lambda<Func<TestEntity, bool>>(Expression.LessThan(prop, biggerValue), param);
178+
var biggerWhere = Expression.Lambda<Func<TestEntity, bool>>(Expression.GreaterThan(prop, smallerValue), param);
179+
var smaller = session.Query<TestEntity>().Single(smallerWhere);
180+
var bigger = session.Query<TestEntity>().Single(biggerWhere);
181+
Assert.That(smaller, Is.Not.Null);
182+
Assert.That(smaller.Id, Is.EqualTo(id));
183+
Assert.That(bigger, Is.Not.Null);
184+
Assert.That(bigger.Id, Is.Not.EqualTo(id));
185+
}
186+
}
187+
188+
[Test]
189+
public virtual void CanQueryProperties()
190+
{
191+
if (PropertiesToTestWithLinq?.Any() != true)
192+
{
193+
Assert.Ignore();
194+
}
195+
196+
var testValue = GetFirstTestValue();
197+
Guid id;
198+
using (var session = OpenSession())
199+
using (var trans = session.BeginTransaction())
200+
{
201+
var entity = new TestEntity { TestProperty = testValue };
202+
session.Save(entity);
203+
id = entity.Id;
204+
trans.Commit();
205+
}
206+
207+
using (var session = OpenSession())
208+
using (var trans = session.BeginTransaction())
209+
{
210+
foreach (var property in PropertiesToTestWithLinq)
211+
{
212+
var param = Expression.Parameter(typeof(TestEntity));
213+
var body = property.Body;
214+
if (body is UnaryExpression unaryExpression)
215+
{
216+
body = unaryExpression.Operand;
217+
}
218+
var member = body as MemberExpression;
219+
if (member is null)
220+
{
221+
Assert.Fail(body + " did not expose a member");
222+
}
223+
var prop = Expression.Property(param, nameof(TestEntity.TestProperty));
224+
var value = property.Compile()(testValue);
225+
var where = Expression.Lambda<Func<TestEntity, bool>>(Expression.Equal(body.Replace(property.Parameters[0], prop), Expression.Constant(value)), param);
226+
TestEntity entity = null;
227+
Assert.DoesNotThrow(() => entity = session.Query<TestEntity>().FirstOrDefault(where), "Unable to query property " + member.Member.Name);
228+
Assert.That(entity, Is.Not.Null, "Unable to query property " + member.Member.Name);
229+
}
230+
}
231+
}
232+
233+
protected override void AddMappings(Configuration configuration)
234+
{
235+
var mapper = new ModelMapper();
236+
237+
mapper.Class<TestEntity>(m =>
238+
{
239+
m.Table("TestEntity");
240+
m.EntityName("TestEntity");
241+
m.Id(p => p.Id, p => p.Generator(Generators.Guid));
242+
m.Property(p => p.TestProperty,
243+
p =>
244+
{
245+
p.Type<TType>();
246+
ConfigurePropertyMapping<TType>(p);
247+
}
248+
);
249+
});
250+
251+
var mapping = mapper.CompileMappingForAllExplicitlyAddedEntities();
252+
configuration.AddMapping(mapping);
253+
}
254+
255+
protected virtual void ConfigurePropertyMapping<TPersistentType>(IPropertyMapper propertyMapper) { }
256+
257+
protected override void OnTearDown()
258+
{
259+
base.OnTearDown();
260+
261+
using var s = OpenSession();
262+
using var t = s.BeginTransaction();
263+
s.Query<TestEntity>().Delete();
264+
t.Commit();
265+
}
266+
public class TestEntity
267+
{
268+
public virtual Guid Id { get; set; }
269+
public virtual TProperty TestProperty { get; set; }
270+
}
271+
272+
public class TypeConfiguration
273+
{
274+
public object Parameters { get; set; }
275+
public short? Scale { get; set; }
276+
public short? Precision { get; set; }
277+
}
278+
}
279+
}

0 commit comments

Comments
 (0)