mirror of
https://github.com/ckaczor/sqltoolsservice.git
synced 2026-01-14 09:59:48 -05:00
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)
389 lines
15 KiB
C#
389 lines
15 KiB
C#
//
|
|
// Copyright (c) Microsoft. All rights reserved.
|
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
//
|
|
|
|
using System;
|
|
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;
|
|
|
|
namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.DataStorage
|
|
{
|
|
public class ReaderWriterPairTest
|
|
{
|
|
[Fact]
|
|
public void ReaderInvalidStreamCannotRead()
|
|
{
|
|
// If: I create a service buffer file stream reader with a stream that cannot be read
|
|
// Then: I should get an exception
|
|
var invalidStream = new Mock<Stream>();
|
|
invalidStream.SetupGet(s => s.CanRead).Returns(false);
|
|
invalidStream.SetupGet(s => s.CanSeek).Returns(true);
|
|
Assert.Throws<InvalidOperationException>(() =>
|
|
{
|
|
ServiceBufferFileStreamReader obj = new ServiceBufferFileStreamReader(invalidStream.Object);
|
|
obj.Dispose();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void ReaderInvalidStreamCannotSeek()
|
|
{
|
|
// If: I create a service buffer file stream reader with a stream that cannot seek
|
|
// Then: I should get an exception
|
|
var invalidStream = new Mock<Stream>();
|
|
invalidStream.SetupGet(s => s.CanRead).Returns(true);
|
|
invalidStream.SetupGet(s => s.CanSeek).Returns(false);
|
|
Assert.Throws<InvalidOperationException>(() =>
|
|
{
|
|
ServiceBufferFileStreamReader obj = new ServiceBufferFileStreamReader(invalidStream.Object);
|
|
obj.Dispose();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void WriterInvalidStreamCannotWrite()
|
|
{
|
|
// If: I create a service buffer file stream writer with a stream that cannot be read
|
|
// Then: I should get an exception
|
|
var invalidStream = new Mock<Stream>();
|
|
invalidStream.SetupGet(s => s.CanWrite).Returns(false);
|
|
invalidStream.SetupGet(s => s.CanSeek).Returns(true);
|
|
Assert.Throws<InvalidOperationException>(() =>
|
|
{
|
|
ServiceBufferFileStreamWriter obj = new ServiceBufferFileStreamWriter(invalidStream.Object, 1024, 1024);
|
|
obj.Dispose();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void WriterInvalidStreamCannotSeek()
|
|
{
|
|
// If: I create a service buffer file stream writer with a stream that cannot seek
|
|
// Then: I should get an exception
|
|
var invalidStream = new Mock<Stream>();
|
|
invalidStream.SetupGet(s => s.CanWrite).Returns(true);
|
|
invalidStream.SetupGet(s => s.CanSeek).Returns(false);
|
|
Assert.Throws<InvalidOperationException>(() =>
|
|
{
|
|
ServiceBufferFileStreamWriter obj = new ServiceBufferFileStreamWriter(invalidStream.Object, 1024, 1024);
|
|
obj.Dispose();
|
|
});
|
|
}
|
|
|
|
private static void VerifyReadWrite<T>(int valueLength, T value, Func<ServiceBufferFileStreamWriter, T, int> writeFunc, Func<ServiceBufferFileStreamReader, FileStreamReadResult> readFunc)
|
|
{
|
|
// Setup: Create a mock file stream
|
|
byte[] storage = new byte[8192];
|
|
|
|
// If:
|
|
// ... I write a type T to the writer
|
|
using (ServiceBufferFileStreamWriter writer = new ServiceBufferFileStreamWriter(new MemoryStream(storage), 10, 10))
|
|
{
|
|
int writtenBytes = writeFunc(writer, value);
|
|
Assert.Equal(valueLength, writtenBytes);
|
|
}
|
|
|
|
// ... And read the type T back
|
|
FileStreamReadResult outValue;
|
|
using (ServiceBufferFileStreamReader reader = new ServiceBufferFileStreamReader(new MemoryStream(storage)))
|
|
{
|
|
outValue = readFunc(reader);
|
|
}
|
|
|
|
// Then:
|
|
Assert.Equal(value, outValue.Value.RawObject);
|
|
Assert.Equal(valueLength, outValue.TotalLength);
|
|
Assert.NotNull(outValue.Value);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0)]
|
|
[InlineData(10)]
|
|
[InlineData(-10)]
|
|
[InlineData(short.MaxValue)] // Two byte number
|
|
[InlineData(short.MinValue)] // Negative two byte number
|
|
public void Int16(short value)
|
|
{
|
|
VerifyReadWrite(sizeof(short) + 1, value, (writer, val) => writer.WriteInt16(val), reader => reader.ReadInt16(0));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0)]
|
|
[InlineData(10)]
|
|
[InlineData(-10)]
|
|
[InlineData(short.MaxValue)] // Two byte number
|
|
[InlineData(short.MinValue)] // Negative two byte number
|
|
[InlineData(int.MaxValue)] // Four byte number
|
|
[InlineData(int.MinValue)] // Negative four byte number
|
|
public void Int32(int value)
|
|
{
|
|
VerifyReadWrite(sizeof(int) + 1, value, (writer, val) => writer.WriteInt32(val), reader => reader.ReadInt32(0));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0)]
|
|
[InlineData(10)]
|
|
[InlineData(-10)]
|
|
[InlineData(short.MaxValue)] // Two byte number
|
|
[InlineData(short.MinValue)] // Negative two byte number
|
|
[InlineData(int.MaxValue)] // Four byte number
|
|
[InlineData(int.MinValue)] // Negative four byte number
|
|
[InlineData(long.MaxValue)] // Eight byte number
|
|
[InlineData(long.MinValue)] // Negative eight byte number
|
|
public void Int64(long value)
|
|
{
|
|
VerifyReadWrite(sizeof(long) + 1, value, (writer, val) => writer.WriteInt64(val), reader => reader.ReadInt64(0));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0)]
|
|
[InlineData(10)]
|
|
public void Byte(byte value)
|
|
{
|
|
VerifyReadWrite(sizeof(byte) + 1, value, (writer, val) => writer.WriteByte(val), reader => reader.ReadByte(0));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData('a')]
|
|
[InlineData('1')]
|
|
[InlineData((char)0x9152)] // Test something in the UTF-16 space
|
|
public void Char(char value)
|
|
{
|
|
VerifyReadWrite(sizeof(char) + 1, value, (writer, val) => writer.WriteChar(val), reader => reader.ReadChar(0));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(true)]
|
|
[InlineData(false)]
|
|
public void Boolean(bool value)
|
|
{
|
|
VerifyReadWrite(sizeof(bool) + 1, value, (writer, val) => writer.WriteBoolean(val), reader => reader.ReadBoolean(0));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0)]
|
|
[InlineData(10.1)]
|
|
[InlineData(-10.1)]
|
|
[InlineData(float.MinValue)]
|
|
[InlineData(float.MaxValue)]
|
|
[InlineData(float.PositiveInfinity)]
|
|
[InlineData(float.NegativeInfinity)]
|
|
public void Single(float value)
|
|
{
|
|
VerifyReadWrite(sizeof(float) + 1, value, (writer, val) => writer.WriteSingle(val), reader => reader.ReadSingle(0));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0)]
|
|
[InlineData(10.1)]
|
|
[InlineData(-10.1)]
|
|
[InlineData(float.MinValue)]
|
|
[InlineData(float.MaxValue)]
|
|
[InlineData(float.PositiveInfinity)]
|
|
[InlineData(float.NegativeInfinity)]
|
|
[InlineData(double.PositiveInfinity)]
|
|
[InlineData(double.NegativeInfinity)]
|
|
[InlineData(double.MinValue)]
|
|
[InlineData(double.MaxValue)]
|
|
public void Double(double value)
|
|
{
|
|
VerifyReadWrite(sizeof(double) + 1, value, (writer, val) => writer.WriteDouble(val), reader => reader.ReadDouble(0));
|
|
}
|
|
|
|
[Fact]
|
|
public void SqlDecimalTest()
|
|
{
|
|
// Setup: Create some test values
|
|
// NOTE: We are doing these here instead of InlineData because SqlDecimal values can't be written as constant expressions
|
|
SqlDecimal[] testValues =
|
|
{
|
|
SqlDecimal.MaxValue, SqlDecimal.MinValue, new SqlDecimal(0x01, 0x01, true, 0, 0, 0, 0)
|
|
};
|
|
foreach (SqlDecimal value in testValues)
|
|
{
|
|
int valueLength = 4 + value.BinData.Length;
|
|
VerifyReadWrite(valueLength, value, (writer, val) => writer.WriteSqlDecimal(val), reader => reader.ReadSqlDecimal(0));
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Decimal()
|
|
{
|
|
// Setup: Create some test values
|
|
// NOTE: We are doing these here instead of InlineData because Decimal values can't be written as constant expressions
|
|
decimal[] testValues =
|
|
{
|
|
decimal.Zero, decimal.One, decimal.MinusOne, decimal.MinValue, decimal.MaxValue
|
|
};
|
|
|
|
foreach (decimal value in testValues)
|
|
{
|
|
int valueLength = decimal.GetBits(value).Length*4 + 1;
|
|
VerifyReadWrite(valueLength, value, (writer, val) => writer.WriteDecimal(val), reader => reader.ReadDecimal(0));
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(3)] // Scale 3 = DATETIME
|
|
[InlineData(7)] // Scale 7 = DATETIME2
|
|
public void DateTimeTest(int scale)
|
|
{
|
|
// 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) + sizeof(int) + 1, value, (writer, val) => writer.WriteDateTime(col, val), reader => reader.ReadDateTime(0));
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void DateTimeOffsetTest()
|
|
{
|
|
// Setup: Create some test values
|
|
// NOTE: We are doing these here instead of InlineData because DateTimeOffset values can't be written as constant expressions
|
|
DateTimeOffset[] testValues =
|
|
{
|
|
DateTimeOffset.Now, DateTimeOffset.UtcNow, DateTimeOffset.MinValue, DateTimeOffset.MaxValue
|
|
};
|
|
foreach (DateTimeOffset value in testValues)
|
|
{
|
|
VerifyReadWrite(sizeof(long)*2 + 1, value, (writer, val) => writer.WriteDateTimeOffset(val), reader => reader.ReadDateTimeOffset(0));
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void TimeSpanTest()
|
|
{
|
|
// Setup: Create some test values
|
|
// NOTE: We are doing these here instead of InlineData because TimeSpan values can't be written as constant expressions
|
|
TimeSpan[] testValues =
|
|
{
|
|
TimeSpan.Zero, TimeSpan.MinValue, TimeSpan.MaxValue, TimeSpan.FromMinutes(60)
|
|
};
|
|
foreach (TimeSpan value in testValues)
|
|
{
|
|
VerifyReadWrite(sizeof(long) + 1, value, (writer, val) => writer.WriteTimeSpan(val), reader => reader.ReadTimeSpan(0));
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void StringNullTest()
|
|
{
|
|
// Setup: Create a mock file stream
|
|
using (MemoryStream stream = new MemoryStream(new byte[8192]))
|
|
{
|
|
// If:
|
|
// ... I write null as a string to the writer
|
|
using (ServiceBufferFileStreamWriter writer = new ServiceBufferFileStreamWriter(stream, 10, 10))
|
|
{
|
|
// Then:
|
|
// ... I should get an argument null exception
|
|
Assert.Throws<ArgumentNullException>(() => writer.WriteString(null));
|
|
}
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0, null)] // Test of empty string
|
|
[InlineData(1, new[] { 'j' })]
|
|
[InlineData(1, new[] { (char)0x9152 })]
|
|
[InlineData(100, new[] { 'j', (char)0x9152 })] // Test alternating utf-16/ascii characters
|
|
[InlineData(512, new[] { 'j', (char)0x9152 })] // Test that requires a 4 byte length
|
|
public void StringTest(int length, char[] values)
|
|
{
|
|
// Setup:
|
|
// ... Generate the test value
|
|
StringBuilder sb = new StringBuilder();
|
|
for (int i = 0; i < length; i++)
|
|
{
|
|
sb.Append(values[i%values.Length]);
|
|
}
|
|
string value = sb.ToString();
|
|
int lengthLength = length == 0 || length > 255 ? 5 : 1;
|
|
VerifyReadWrite(sizeof(char)*length + lengthLength, value, (writer, val) => writer.WriteString(value), reader => reader.ReadString(0));
|
|
}
|
|
|
|
[Fact]
|
|
public void BytesNullTest()
|
|
{
|
|
// Setup: Create a mock file stream wrapper
|
|
using (MemoryStream stream = new MemoryStream(new byte[8192]))
|
|
{
|
|
// If:
|
|
// ... I write null as a string to the writer
|
|
using (ServiceBufferFileStreamWriter writer = new ServiceBufferFileStreamWriter(stream, 10, 10))
|
|
{
|
|
// Then:
|
|
// ... I should get an argument null exception
|
|
Assert.Throws<ArgumentNullException>(() => writer.WriteBytes(null));
|
|
}
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0, new byte[] { 0x00 })] // Test of empty byte[]
|
|
[InlineData(1, new byte[] { 0x00 })]
|
|
[InlineData(1, new byte[] { 0xFF })]
|
|
[InlineData(100, new byte[] { 0x10, 0xFF, 0x00 })]
|
|
[InlineData(512, new byte[] { 0x10, 0xFF, 0x00 })] // Test that requires a 4 byte length
|
|
public void Bytes(int length, byte[] values)
|
|
{
|
|
// Setup:
|
|
// ... Generate the test value
|
|
List<byte> sb = new List<byte>();
|
|
for (int i = 0; i < length; i++)
|
|
{
|
|
sb.Add(values[i % values.Length]);
|
|
}
|
|
byte[] value = sb.ToArray();
|
|
int lengthLength = length == 0 || length > 255 ? 5 : 1;
|
|
int valueLength = sizeof(byte)*length + lengthLength;
|
|
VerifyReadWrite(valueLength, value, (writer, val) => writer.WriteBytes(value), reader => reader.ReadBytes(0));
|
|
}
|
|
|
|
[Fact]
|
|
public void GuidTest()
|
|
{
|
|
// Setup:
|
|
// ... Create some test values
|
|
// NOTE: We are doing these here instead of InlineData because Guid type can't be written as constant expressions
|
|
Guid[] guids =
|
|
{
|
|
Guid.Empty, Guid.NewGuid(), Guid.NewGuid()
|
|
};
|
|
foreach (Guid guid in guids)
|
|
{
|
|
VerifyReadWrite(guid.ToByteArray().Length + 1, new SqlGuid(guid), (writer, val) => writer.WriteGuid(guid), reader => reader.ReadGuid(0));
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void MoneyTest()
|
|
{
|
|
// Setup: Create some test values
|
|
// NOTE: We are doing these here instead of InlineData because SqlMoney can't be written as a constant expression
|
|
SqlMoney[] monies =
|
|
{
|
|
SqlMoney.Zero, SqlMoney.MinValue, SqlMoney.MaxValue, new SqlMoney(1.02)
|
|
};
|
|
foreach (SqlMoney money in monies)
|
|
{
|
|
VerifyReadWrite(sizeof(decimal) + 1, money, (writer, val) => writer.WriteMoney(money), reader => reader.ReadMoney(0));
|
|
}
|
|
}
|
|
}
|
|
}
|