diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/ServiceBufferFileStreamReader.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/ServiceBufferFileStreamReader.cs index f662a5d5..7acb9559 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/ServiceBufferFileStreamReader.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/ServiceBufferFileStreamReader.cs @@ -81,7 +81,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage {typeof(float), (o, id, col) => ReadSingle(o, id)}, {typeof(decimal), (o, id, col) => ReadDecimal(o, id)}, {typeof(DateTime), ReadDateTime}, - {typeof(DateTimeOffset), (o, id, col) => ReadDateTimeOffset(o, id)}, + {typeof(DateTimeOffset), ReadDateTimeOffset}, {typeof(TimeSpan), (o, id, col) => ReadTimeSpan(o, id)}, {typeof(byte[]), (o, id, col) => ReadBytes(o, id)}, {typeof(Guid), (o, id, col) => ReadGuid(o, id)}, @@ -454,8 +454,9 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage /// /// Offset into the file to read the DateTimeOffset from /// Internal ID of the row that will be stored in the cell + /// Column metadata, used for determining what precision to output /// A DateTimeOffset - internal FileStreamReadResult ReadDateTimeOffset(long offset, long rowId) + internal FileStreamReadResult ReadDateTimeOffset(long offset, long rowId, DbColumnWrapper col) { // DateTimeOffset is represented by DateTime.Ticks followed by TimeSpan.Ticks // both as Int64 values @@ -466,8 +467,14 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage return new DateTimeOffset(new DateTime(dtTicks), new TimeSpan(dtOffset)); }, null, dt => { - string formatString = $"{DateFormatString} {TimeFormatString}.fffffff zzz"; - + int scale = Math.Min(col.NumericScale ?? 7, 7); + string formatString = $"{DateFormatString} {TimeFormatString}"; + if (scale > 0) + { + string millisecondString = new string('f', scale); + formatString += $".{millisecondString}"; + } + formatString += " zzz"; return dt.ToString(formatString); }); } diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/QueryExecution/DataTypeTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/QueryExecution/DataTypeTests.cs index fe8a645c..815751bd 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/QueryExecution/DataTypeTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/QueryExecution/DataTypeTests.cs @@ -68,6 +68,8 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.QueryExecution public async Task DateTimeOffsetTest() { await ExecuteAndVerifyResult("SELECT CAST('2020-01-01' AS DATETIMEOFFSET)", "2020-01-01 00:00:00.0000000 +00:00"); + await ExecuteAndVerifyResult("SELECT CAST('2020-01-01' AS DATETIMEOFFSET(6))", "2020-01-01 00:00:00.000000 +00:00"); + await ExecuteAndVerifyResult("SELECT CAST('2020-01-01' AS DATETIMEOFFSET(0))", "2020-01-01 00:00:00 +00:00"); } [Test] diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/QueryExecution/DataStorage/ServiceBufferFileStreamReaderWriterTests.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/QueryExecution/DataStorage/ServiceBufferFileStreamReaderWriterTests.cs index 26a6a74d..cafd7f48 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/QueryExecution/DataStorage/ServiceBufferFileStreamReaderWriterTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/QueryExecution/DataStorage/ServiceBufferFileStreamReaderWriterTests.cs @@ -432,14 +432,39 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.QueryExecution.DataStorage { DateTimeOffset.Now, DateTimeOffset.UtcNow, DateTimeOffset.MinValue, DateTimeOffset.MaxValue }; + + // Setup: Create a DATETIMEOFFSET column + DbColumnWrapper col = new DbColumnWrapper(new TestDbColumn { DataTypeName = "datetimeoffset", NumericScale = 6 }); + foreach (DateTimeOffset value in testValues) { string displayValue = VerifyReadWrite(sizeof(long)*2 + 1, value, (writer, val) => writer.WriteDateTimeOffset(val), - (reader, rowId) => reader.ReadDateTimeOffset(0, rowId)); + (reader, rowId) => reader.ReadDateTimeOffset(0, rowId, col)); - // Make sure the display value has a time string with 7 milliseconds and a time zone - Assert.True(Regex.IsMatch(displayValue, @"^[\d]{4}-[\d]{2}-[\d]{2} [\d]{2}:[\d]{2}:[\d]{2}\.[\d]{7} [+-][01][\d]:[\d]{2}$")); + // Make sure the display value has a time string with 6 milliseconds and a time zone + Assert.True(Regex.IsMatch(displayValue, @"^[\d]{4}-[\d]{2}-[\d]{2} [\d]{2}:[\d]{2}:[\d]{2}\.[\d]{6} [+-][01][\d]:[\d]{2}$")); + } + } + [Test] + public void DateTimeOffsetZeroScaleTest() + { + // Setup: Create some test values + DateTimeOffset[] testValues = + { + DateTimeOffset.Now, DateTimeOffset.UtcNow, DateTimeOffset.MinValue, DateTimeOffset.MaxValue + }; + + // Setup: Create a DATETIMEOFFSET column + DbColumnWrapper col = new DbColumnWrapper(new TestDbColumn { DataTypeName = "datetimeoffset", NumericScale = 0 }); + + foreach (DateTimeOffset value in testValues) + { + string displayValue = VerifyReadWrite(sizeof(long) * 2 + 1, value, (writer, val) => writer.WriteDateTimeOffset(val), + (reader, rowId) => reader.ReadDateTimeOffset(0, rowId, col)); + + // Make sure the display value has a time string with no millisecond and a time zone + Assert.True(Regex.IsMatch(displayValue, @"^[\d]{4}-[\d]{2}-[\d]{2} [\d]{2}:[\d]{2}:[\d]{2} [+-][01][\d]:[\d]{2}$")); } }