From 431dfa41565412dc6ddb8cc243b286f668ef7f45 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Fri, 9 Dec 2016 12:46:17 -0800 Subject: [PATCH] Adding Milliseconds to DateTime fields (#173) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a slightly larger change than anticipated due the difference between `DATETIME`, `DATETIME2`, and `DateTime`. The `DATETIME` type always uses 3 decimal points of a second, while the `DATETIME2` type has 7 (although since `DATETIME2(7)` is default in SSMS suggesting that it is a variable precision type). Regardless of the db type, the engine returns `DateTime` C# type. The db types are only made visible via the column info, via the numeric precision and numeric scale. My findings were as such: `DATETIME `: Precision = 23, Scale = 3 `DATETIME2`: Precision = 255, Scale = 7 The scale corresponds neatly with the number of second decimal points to show. The buffer file writer was modified to store both the scale and the number of ticks. Then the buffer file reader was modified to read in the precision and the number of ticks and generate the ToString version of the DateTime to add "f" as many times as there is scale, which corresponds to milliseconds. * Code for writing milliseconds of datetime/datetime2 columns * Adding unit tests * Fixing potential bug with datetime2(0) --- .../DataStorage/IFileStreamWriter.cs | 3 +- .../ServiceBufferFileStreamReader.cs | 25 +++- .../ServiceBufferFileStreamWriter.cs | 124 +++++++++++++----- ...erviceBufferFileStreamReaderWriterTests.cs | 13 +- .../Utility/TestDbColumn.cs | 6 + 5 files changed, 126 insertions(+), 45 deletions(-) diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/IFileStreamWriter.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/IFileStreamWriter.cs index 7cfffee8..951bd89c 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/IFileStreamWriter.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/IFileStreamWriter.cs @@ -5,6 +5,7 @@ using System; using System.Data.SqlTypes; +using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage { @@ -25,7 +26,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage int WriteDouble(double val); int WriteDecimal(decimal val); int WriteSqlDecimal(SqlDecimal val); - int WriteDateTime(DateTime val); + int WriteDateTime(DbColumnWrapper column, DateTime val); int WriteDateTimeOffset(DateTimeOffset dtoVal); int WriteTimeSpan(TimeSpan val); int WriteString(string val); diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/ServiceBufferFileStreamReader.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/ServiceBufferFileStreamReader.cs index 5df33596..487f827b 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/ServiceBufferFileStreamReader.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/ServiceBufferFileStreamReader.cs @@ -260,11 +260,26 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage /// A DateTime public FileStreamReadResult ReadDateTime(long offset) { - return ReadCellHelper(offset, length => - { - long ticks = BitConverter.ToInt64(buffer, 0); - return new DateTime(ticks); - }); + int precision = 0; + + return ReadCellHelper(offset, + length => + { + precision = BitConverter.ToInt32(buffer, 0); + long ticks = BitConverter.ToInt64(buffer, 4); + return new DateTime(ticks); + }, null, + time => + { + string format = "yyyy-MM-dd HH:mm:ss"; + if (precision > 0) + { + // Output the number milliseconds equivalent to the precision + // NOTE: string('f', precision) will output ffff for precision=4 + format += "." + new string('f', precision); + } + return time.ToString(format); + }); } /// diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/ServiceBufferFileStreamWriter.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/ServiceBufferFileStreamWriter.cs index bb7167e9..75045aff 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/ServiceBufferFileStreamWriter.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/ServiceBufferFileStreamWriter.cs @@ -38,7 +38,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage /// /// Functions to use for writing various types to a file /// - private readonly Dictionary> writeMethods; + private readonly Dictionary> writeMethods; #endregion @@ -74,37 +74,78 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage this.maxXmlCharsToStore = maxXmlCharsToStore; // Define what methods to use to write a type to the file - writeMethods = new Dictionary> + writeMethods = new Dictionary> { - {typeof(string), val => WriteString((string) val)}, - {typeof(short), val => WriteInt16((short) val)}, - {typeof(int), val => WriteInt32((int) val)}, - {typeof(long), val => WriteInt64((long) val)}, - {typeof(byte), val => WriteByte((byte) val)}, - {typeof(char), val => WriteChar((char) val)}, - {typeof(bool), val => WriteBoolean((bool) val)}, - {typeof(double), val => WriteDouble((double) val) }, - {typeof(float), val => WriteSingle((float) val) }, - {typeof(decimal), val => WriteDecimal((decimal) val) }, - {typeof(DateTime), val => WriteDateTime((DateTime) val) }, - {typeof(DateTimeOffset), val => WriteDateTimeOffset((DateTimeOffset) val) }, - {typeof(TimeSpan), val => WriteTimeSpan((TimeSpan) val) }, - {typeof(byte[]), val => WriteBytes((byte[]) val)}, + {typeof(string), (val, col) => WriteString((string) val)}, + {typeof(short), (val, col) => WriteInt16((short) val)}, + {typeof(int), (val, col) => WriteInt32((int) val)}, + {typeof(long), (val, col) => WriteInt64((long) val)}, + {typeof(byte), (val, col) => WriteByte((byte) val)}, + {typeof(char), (val, col) => WriteChar((char) val)}, + {typeof(bool), (val, col) => WriteBoolean((bool) val)}, + {typeof(double), (val, col) => WriteDouble((double) val) }, + {typeof(float), (val, col) => WriteSingle((float) val) }, + {typeof(decimal), (val, col) => WriteDecimal((decimal) val) }, + {typeof(DateTime), (val, col) => WriteDateTime(col, (DateTime) val) }, + {typeof(DateTimeOffset), (val, col) => WriteDateTimeOffset((DateTimeOffset) val) }, + {typeof(TimeSpan), (val, col) => WriteTimeSpan((TimeSpan) val) }, + {typeof(byte[]), (val, col) => WriteBytes((byte[]) val)}, - {typeof(SqlString), val => WriteNullable((SqlString) val, obj => WriteString((string) obj))}, - {typeof(SqlInt16), val => WriteNullable((SqlInt16) val, obj => WriteInt16((short) obj))}, - {typeof(SqlInt32), val => WriteNullable((SqlInt32) val, obj => WriteInt32((int) obj))}, - {typeof(SqlInt64), val => WriteNullable((SqlInt64) val, obj => WriteInt64((long) obj)) }, - {typeof(SqlByte), val => WriteNullable((SqlByte) val, obj => WriteByte((byte) obj)) }, - {typeof(SqlBoolean), val => WriteNullable((SqlBoolean) val, obj => WriteBoolean((bool) obj)) }, - {typeof(SqlDouble), val => WriteNullable((SqlDouble) val, obj => WriteDouble((double) obj)) }, - {typeof(SqlSingle), val => WriteNullable((SqlSingle) val, obj => WriteSingle((float) obj)) }, - {typeof(SqlDecimal), val => WriteNullable((SqlDecimal) val, obj => WriteSqlDecimal((SqlDecimal) obj)) }, - {typeof(SqlDateTime), val => WriteNullable((SqlDateTime) val, obj => WriteDateTime((DateTime) obj)) }, - {typeof(SqlBytes), val => WriteNullable((SqlBytes) val, obj => WriteBytes((byte[]) obj)) }, - {typeof(SqlBinary), val => WriteNullable((SqlBinary) val, obj => WriteBytes((byte[]) obj)) }, - {typeof(SqlGuid), val => WriteNullable((SqlGuid) val, obj => WriteGuid((Guid) obj)) }, - {typeof(SqlMoney), val => WriteNullable((SqlMoney) val, obj => WriteMoney((SqlMoney) obj)) } + { + typeof(SqlString), + (val, col) => WriteNullable((SqlString) val, obj => WriteString((string) obj)) + }, + { + typeof(SqlInt16), + (val, col) => WriteNullable((SqlInt16) val, obj => WriteInt16((short) obj)) + }, + { + typeof(SqlInt32), + (val, col) => WriteNullable((SqlInt32) val, obj => WriteInt32((int) obj)) + }, + { + typeof(SqlInt64), + (val, col) => WriteNullable((SqlInt64) val, obj => WriteInt64((long) obj)) + }, + { + typeof(SqlByte), + (val, col) => WriteNullable((SqlByte) val, obj => WriteByte((byte) obj)) + }, + { + typeof(SqlBoolean), + (val, col) => WriteNullable((SqlBoolean) val, obj => WriteBoolean((bool) obj)) }, + { + typeof(SqlDouble), + (val, col) => WriteNullable((SqlDouble) val, obj => WriteDouble((double) obj)) + }, + { + typeof(SqlSingle), + (val, col) => WriteNullable((SqlSingle) val, obj => WriteSingle((float) obj)) + }, + { + typeof(SqlDecimal), + (val, col) => WriteNullable((SqlDecimal) val, obj => WriteSqlDecimal((SqlDecimal) obj)) + }, + { + typeof(SqlDateTime), + (val, col) => WriteNullable((SqlDateTime) val, obj => WriteDateTime(col, (DateTime) obj)) + }, + { + typeof(SqlBytes), + (val, col) => WriteNullable((SqlBytes) val, obj => WriteBytes((byte[]) obj)) + }, + { + typeof(SqlBinary), + (val, col) => WriteNullable((SqlBinary) val, obj => WriteBytes((byte[]) obj)) + }, + { + typeof(SqlGuid), + (val, col) => WriteNullable((SqlGuid) val, obj => WriteGuid((Guid) obj)) + }, + { + typeof(SqlMoney), + (val, col) => WriteNullable((SqlMoney) val, obj => WriteMoney((SqlMoney) obj)) + } }; } @@ -190,10 +231,10 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage } // Use the appropriate writing method for the type - Func writeMethod; + Func writeMethod; if (writeMethods.TryGetValue(tVal, out writeMethod)) { - rowBytes += writeMethod(values[i]); + rowBytes += writeMethod(values[i], ci); } else { @@ -354,12 +395,25 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage } /// - /// Writes a DateTime to the file + /// Writes a DateTime to the file as precision and ticks /// /// Number of bytes used to store the DateTime - public int WriteDateTime(DateTime dtVal) + public int WriteDateTime(DbColumnWrapper col, DateTime dtVal) { - return WriteInt64(dtVal.Ticks); + // Length + var length = WriteLength(12); + + // Precision + intBuffer[0] = col.NumericScale ?? 3; + Buffer.BlockCopy(intBuffer, 0, byteBuffer, 0, 4); + + // Ticks + longBuffer[0] = dtVal.Ticks; + Buffer.BlockCopy(longBuffer, 0, byteBuffer, 4, 8); + + length += WriteHelper(byteBuffer, 12); + + return length; } /// diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/DataStorage/ServiceBufferFileStreamReaderWriterTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/DataStorage/ServiceBufferFileStreamReaderWriterTests.cs index b454eafb..40496d63 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/DataStorage/ServiceBufferFileStreamReaderWriterTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/DataStorage/ServiceBufferFileStreamReaderWriterTests.cs @@ -8,7 +8,9 @@ using System.Collections.Generic; using System.Data.SqlTypes; using System.IO; using System.Text; +using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; using Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage; +using Microsoft.SqlTools.ServiceLayer.Test.Utility; using Moq; using Xunit; @@ -229,18 +231,21 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.DataStorage } } - [Fact] - public void DateTimeTest() + [Theory] + [InlineData(3)] // Scale 3 = DATETIME + [InlineData(7)] // Scale 7 = DATETIME2 + public void DateTimeTest(int scale) { - // Setup: Create some test values + // Setup: Create some test values and a column with scale set // NOTE: We are doing these here instead of InlineData because DateTime values can't be written as constant expressions + DbColumnWrapper col = new DbColumnWrapper(new TestDbColumn("dbcol", scale)); DateTime[] testValues = { DateTime.Now, DateTime.UtcNow, DateTime.MinValue, DateTime.MaxValue }; foreach (DateTime value in testValues) { - VerifyReadWrite(sizeof(long) + 1, value, (writer, val) => writer.WriteDateTime(val), reader => reader.ReadDateTime(0)); + VerifyReadWrite(sizeof(long) + sizeof(int) + 1, value, (writer, val) => writer.WriteDateTime(col, val), reader => reader.ReadDateTime(0)); } } diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/Utility/TestDbColumn.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/Utility/TestDbColumn.cs index b8b93276..00e88637 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/Utility/TestDbColumn.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/Utility/TestDbColumn.cs @@ -17,5 +17,11 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.Utility base.DataType = typeof(string); base.DataTypeName = "nvarchar"; } + + public TestDbColumn(string columnName, int numericScale) + : this(columnName) + { + base.NumericScale = numericScale; + } } }