Skip to content

Commit

Permalink
SpannerDate and its unit tests, without breaking changes to existing …
Browse files Browse the repository at this point in the history
…DateTime for Date.
  • Loading branch information
Rishabh-V committed Apr 25, 2022
1 parent d654082 commit 4e3b94e
Show file tree
Hide file tree
Showing 5 changed files with 512 additions and 10 deletions.
Expand Up @@ -12,8 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.

using Google.Cloud.Spanner.V1;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Xunit;

namespace Google.Cloud.Spanner.Data.IntegrationTests
Expand All @@ -27,6 +30,12 @@ public class DateTimestampReadTests
public DateTimestampReadTests(DateTimestampTableFixture fixture) =>
_fixture = fixture;

private static bool[] s_readWriteAsDateTime = new[]
{
true,
false
};

public static IEnumerable<object[]> TestDates =>
new List<object[]>
{
Expand All @@ -38,22 +47,70 @@ public class DateTimestampReadTests
new object[] { new DateTime(2020, 12, 29, 0, 0, 0, DateTimeKind.Local) },
};

public static IEnumerable<object[]> TestDatesWithReadWriteTypes() =>
from writeAsDateTime in s_readWriteAsDateTime
from readAsDateTime in s_readWriteAsDateTime
from parameters in TestDates
select new object[] { writeAsDateTime, readAsDateTime }.Concat(parameters).ToArray();

[Theory]
[MemberData(nameof(TestDates))]
public async void WriteDateThenRead_ShouldBeEqual(DateTime expectedDate)
public async Task WriteDateThenRead_ShouldBeEqual(DateTime expectedDate)
{
using (var connection = _fixture.GetConnection())
{
// Write the date value and read it back.
await connection.CreateInsertOrUpdateCommand(_fixture.TableName,
new SpannerParameterCollection
{
using var connection = _fixture.GetConnection();
// Write the date value and read it back.
await connection.CreateInsertOrUpdateCommand(_fixture.TableName,
new SpannerParameterCollection
{
new SpannerParameter("DateValue", SpannerDbType.Date, expectedDate),
}
).ExecuteNonQueryAsync();
}
).ExecuteNonQueryAsync();
var dbDate = await connection.CreateSelectCommand($"SELECT DateValue FROM {_fixture.TableName}").ExecuteScalarAsync<DateTime>();
Assert.Equal(expectedDate, dbDate);
}

/// <summary>
/// Writes the date then reads it using DateTime or SpannerDate based on parameters and validates that
/// values are equal.
/// </summary>
/// <param name="writeAsDateTime">if set to <c>true</c> <see cref="DateTime"/> is used to write the data, else <see cref="SpannerDate"/> is used.</param>
/// <param name="readAsDateTime">if set to <c>true</c> <see cref="DateTime"/> is used to read the data, else <see cref="SpannerDate"/> is used.</param>
/// <param name="expectedDate">The expected date.</param>
[Theory]
[MemberData(nameof(TestDatesWithReadWriteTypes))]
public async Task WriteDateThenRead_ShouldBeEqual_UseSpannerDateAndDateTime(bool writeAsDateTime,
bool readAsDateTime,
DateTime expectedDate)
{
// Adding a new SpannerDate type in backward compatible fashion.
// We could write date as SpannerDate and read as DateTime or
// write date as DateTime and read as SpannerDate. This is in addition to
// write date as DateTime and read as DateTime or,
// write date as SpannerDate and read as SpannerDate.
using var connection = _fixture.GetConnection();
// Write the date value and read it back.
await connection.CreateInsertOrUpdateCommand(_fixture.TableName,
writeAsDateTime ?
new SpannerParameterCollection
{
new SpannerParameter("DateValue", SpannerDbType.Date, expectedDate)
}
: new SpannerParameterCollection
{
new SpannerParameter("DateValue", SpannerDbType.Date, SpannerDate.FromDateTime(expectedDate))
}
).ExecuteNonQueryAsync();

if (readAsDateTime)
{
var dbDate = await connection.CreateSelectCommand($"SELECT DateValue FROM {_fixture.TableName}").ExecuteScalarAsync<DateTime>();
Assert.Equal(expectedDate, dbDate);
}
else
{
var dbDate = await connection.CreateSelectCommand($"SELECT DateValue FROM {_fixture.TableName}").ExecuteScalarAsync<SpannerDate>();
Assert.Equal(SpannerDate.FromDateTime(expectedDate), dbDate);
}
}

public static IEnumerable<object[]> TestTimestamps =>
Expand All @@ -68,7 +125,7 @@ public async void WriteDateThenRead_ShouldBeEqual(DateTime expectedDate)

[Theory]
[MemberData(nameof(TestTimestamps))]
public async void WriteTimestampThenRead_ShouldBeEqual(DateTime expectedTimestamp)
public async Task WriteTimestampThenRead_ShouldBeEqual(DateTime expectedTimestamp)
{
using (var connection = _fixture.GetConnection())
{
Expand Down
@@ -0,0 +1,160 @@
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using System;
using System.Collections.Generic;
using System.Globalization;
using Xunit;

namespace Google.Cloud.Spanner.V1.Tests
{
public class SpannerDateTests
{
public static IEnumerable<object[]> FromDateTimeData()
{
// Input: DateTime values.
yield return new object[] { new DateTime(2022, 4, 18, 0, 0, 0, DateTimeKind.Utc) };
yield return new object[] { new DateTime(2022, 4, 18, 1, 0, 0, DateTimeKind.Utc) };
yield return new object[] { new DateTime(2022, 4, 18, 0, 0, 0, DateTimeKind.Local) };
yield return new object[] { new DateTime(2022, 4, 18, 1, 0, 0, DateTimeKind.Local) };
yield return new object[] { new DateTime(2022, 4, 18, 23, 59, 59, DateTimeKind.Utc) };
yield return new object[] { new DateTime(2022, 4, 18, 23, 59, 59, DateTimeKind.Local) };
yield return new object[] { new DateTime(2022, 4, 18, 23, 59, 59, DateTimeKind.Unspecified) };
yield return new object[] { new DateTime(0001, 01, 01, 0, 0, 0, DateTimeKind.Utc) };
yield return new object[] { new DateTime(9999, 12, 31, 23, 59, 59, DateTimeKind.Utc) };
yield return new object[] { new DateTime(2000, 12, 31, 23, 59, 59, DateTimeKind.Unspecified) };
}

[Theory]
[InlineData("0001-01-01")]
[InlineData("9999-12-31")]
[InlineData("2022-04-18")]
[InlineData("2020-02-29")]
public void ParseToStringRoundTrip(string input)
{
SpannerDate date = SpannerDate.Parse(input);
Assert.Equal(input, date.ToString());

// Check that TryParse works too.
Assert.True(SpannerDate.TryParse(input, out var date2));
Assert.Equal(date, date2);
}

[Theory]
[InlineData("")]
[InlineData(" ")]
[InlineData("ABCDEFGH")]
[InlineData("pp/qq/rrss")]
[InlineData("12-34-5678")]
[InlineData("0000-01-01")]
[InlineData("9999-12-32")]
[InlineData("0000/01/01")]
[InlineData("9999/12/32")]
[InlineData("2020-00-01")]
[InlineData("2020-13-13")]
[InlineData("2020-01-00")]
[InlineData("2020-01-32")]
[InlineData("2020-02-30")]
[InlineData("12/31/9999")]
[InlineData("0001/01/01")]
[InlineData("2022/04/20")]
[InlineData("31/12/9999")]
public void Parse_Invalid(string input)
{
Assert.Throws<FormatException>(() => SpannerDate.Parse(input));
bool success = SpannerDate.TryParse(input, out SpannerDate date2);
Assert.Equal(default(SpannerDate), date2);
Assert.False(success);
}

[Theory]
[InlineData("0001-01-01","0001-01-02")]
[InlineData("9999-12-30", "9999-12-31")]
[InlineData("0001-01-01", "2022-04-18")]
[InlineData("2022-04-18", "9999-12-31")]
public void Inequality(string input1, string input2)
{
var date1 = SpannerDate.Parse(input1);
var date2 = SpannerDate.Parse(input2);
// Value of input1 is always less than input2.
// So, we assert that input1 is not equal to input2 and
// input1 is less than input2 and input2 is greater than input1.
// In addition to inequality, we assert that first date
// is less than second date using Assert.True.
Assert.NotEqual(date1, date2);
Assert.False(date1 == date2);
Assert.NotEqual(date1.GetHashCode(), date2.GetHashCode());
Assert.False(date1.GetHashCode() == date2.GetHashCode());
Assert.True(date1 != date2);
Assert.True(!date1.Equals(date2));
Assert.True(!date2.Equals(date1));
Assert.True(date1 < date2);
Assert.True(date1 <= date2);
Assert.True(date1.CompareTo(date2) < 0);
Assert.True(date2 > date1);
Assert.True(date2 >= date1);
Assert.True(date2.CompareTo(date1) > 0);
}

[Theory]
[InlineData("0001-01-01")]
[InlineData("9999-12-31")]
[InlineData("2022-04-18")]
[InlineData("2000-02-28")]
public void Equality(string input1)
{
var date1 = SpannerDate.Parse(input1);
var date2 = SpannerDate.Parse(input1);
var hashCode1 = date1.GetHashCode();
var hashCode2 = date2.GetHashCode();
Assert.True(date1.Equals(date2));
Assert.True(date2.Equals(date1));
Assert.Equal(0, date1.CompareTo(date2));
Assert.Equal(0, date2.CompareTo(date1));
Assert.Equal(hashCode1, hashCode2);
}

[Theory]
[MemberData(nameof(FromDateTimeData))]
public void FromDateTime(DateTime input)
{
SpannerDate spannerDate = SpannerDate.FromDateTime(input);
Assert.Equal(input.Date.Year, spannerDate.Year);
Assert.Equal(input.Date.Month, spannerDate.Month);
Assert.Equal(input.Date.Day, spannerDate.Day);
}

[Theory]
[InlineData("9999-12-31")]
[InlineData("0001-01-01")]
[InlineData("2022-04-18")]
[InlineData("2000-12-31")]
public void ToDateTime(string input)
{
SpannerDate spannerDate = SpannerDate.Parse(input);
var expectedDateTime = DateTime.Parse(input, CultureInfo.InvariantCulture);
var result = spannerDate.ToDateTime();
Assert.Equal(expectedDateTime, result);
Assert.Equal(DateTimeKind.Unspecified, result.Kind);
}

[Fact]
public void Constants()
{
Assert.Equal(SpannerDate.FromDateTime(DateTime.MaxValue), SpannerDate.MaxDate);
Assert.Equal(SpannerDate.FromDateTime(DateTime.MinValue), SpannerDate.MinDate);
Assert.Equal(default(SpannerDate), SpannerDate.Parse("0001-01-01"));
}
}
}
Expand Up @@ -236,6 +236,13 @@ public override long GetBytes(int ordinal, long fieldOffset, byte[] buffer, int
/// <returns>The value converted to a <see cref="PgNumeric"/>.</returns>
public PgNumeric GetPgNumeric(int i) => GetFieldValue<PgNumeric>(i);

/// <summary>
/// Gets the value of the specified column as type <see cref="SpannerDate"/>.
/// </summary>
/// <param name="i">The index of the column to retrieve.</param>
/// <returns>The value converted to a <see cref="SpannerDate"/>.</returns>
public SpannerDate GetSpannerDate(int i) => GetFieldValue<SpannerDate>(i);

/// <inheritdoc />
public override object GetValue(int i) => this[i];

Expand Down
Expand Up @@ -179,6 +179,10 @@ internal Value ToProtobufValue(object value, SpannerConversionOptions options)
StringValue = XmlConvert.ToString(Convert.ToDateTime(value, InvariantCulture), XmlDateTimeSerializationMode.Utc)
};
case TypeCode.Date:
if(value is SpannerDate spannerDate)
{
return new Value { StringValue = spannerDate.ToString() };
}
if (value is DateTime date && date.Kind == DateTimeKind.Local)
{
// If the DateTime is local the value should be changed to unspecified
Expand Down Expand Up @@ -438,6 +442,20 @@ private object ConvertToClrTypeImpl(Value wireValue, System.Type targetClrType,
}
}

if (targetClrType == typeof(SpannerDate))
{
switch (wireValue.KindCase)
{
case Value.KindOneofCase.NullValue:
return null;
case Value.KindOneofCase.StringValue:
return SpannerDate.Parse(wireValue.StringValue);
default:
throw new InvalidOperationException(
$"Invalid Type conversion from {wireValue.KindCase} to {targetClrType.FullName}");
}
}

if (targetClrType == typeof(Timestamp))
{
switch (wireValue.KindCase)
Expand Down

0 comments on commit 4e3b94e

Please sign in to comment.