Improves "save as Excel" functionality (#2266)

This commit is contained in:
2023-10-11 19:32:04 -04:00
committed by GitHub
parent a3b42e43bd
commit ad0aedb0a8
15 changed files with 969 additions and 121 deletions

View File

@@ -57,6 +57,8 @@
<PackageReference Update="coverlet.collector" Version="6.0.0" /> <PackageReference Update="coverlet.collector" Version="6.0.0" />
<PackageReference Update="coverlet.msbuild" Version="6.0.0" /> <PackageReference Update="coverlet.msbuild" Version="6.0.0" />
<PackageReference Update="Microsoft.SqlServer.XEvent.XELite" Version="2023.1.30.3" /> <PackageReference Update="Microsoft.SqlServer.XEvent.XELite" Version="2023.1.30.3" />
<PackageReference Update="SkiaSharp" Version="2.88.6" />
<PackageReference Update="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="2.88.6" Condition="$([MSBuild]::IsOsPlatform('Linux'))" />
</ItemGroup> </ItemGroup>
<!-- When updating version of Dependencies in the below section, please also update the version in the following files: <!-- When updating version of Dependencies in the below section, please also update the version in the following files:

View File

@@ -50,6 +50,8 @@
<PackageReference Include="Microsoft.SqlServer.Management.SmoMetadataProvider" /> <PackageReference Include="Microsoft.SqlServer.Management.SmoMetadataProvider" />
<PackageReference Include="Microsoft.SqlServer.SqlManagementObjects" /> <PackageReference Include="Microsoft.SqlServer.SqlManagementObjects" />
<PackageReference Include="Microsoft.SqlServer.Management.SqlParser" /> <PackageReference Include="Microsoft.SqlServer.Management.SqlParser" />
<PackageReference Include="SkiaSharp" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Condition="$([MSBuild]::IsOsPlatform('Linux'))" />
<PackageReference Include="System.Configuration.ConfigurationManager" /> <PackageReference Include="System.Configuration.ConfigurationManager" />
<PackageReference Include="System.Text.Encoding.CodePages" /> <PackageReference Include="System.Text.Encoding.CodePages" />
<PackageReference Include="Microsoft.SqlServer.XEvent.XELite" /> <PackageReference Include="Microsoft.SqlServer.XEvent.XELite" />

View File

@@ -115,6 +115,26 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts
/// Include headers of columns in Excel /// Include headers of columns in Excel
/// </summary> /// </summary>
public bool IncludeHeaders { get; set; } public bool IncludeHeaders { get; set; }
/// <summary>
/// Freeze header row in Excel
/// </summary>
public bool FreezeHeaderRow { get; set; }
/// <summary>
/// Bold header row in Excel
/// </summary>
public bool BoldHeaderRow { get; set; }
/// <summary>
/// Enable auto filter on header row in Excel
/// </summary>
public bool AutoFilterHeaderRow { get; set; }
/// <summary>
/// Auto size columns in Excel
/// </summary>
public bool AutoSizeColumns { get; set; }
} }
/// <summary> /// <summary>

View File

@@ -157,6 +157,30 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts
get { return this.GetOptionValue<int>(SerializationOptionsHelper.MaxCharsToStore); } get { return this.GetOptionValue<int>(SerializationOptionsHelper.MaxCharsToStore); }
set { this.SetOptionValue<int>(SerializationOptionsHelper.Formatted, value); } set { this.SetOptionValue<int>(SerializationOptionsHelper.Formatted, value); }
} }
public bool FreezeHeaderRow
{
get { return this.GetOptionValue<bool>(SerializationOptionsHelper.FreezeHeaderRow); }
set { this.SetOptionValue<bool>(SerializationOptionsHelper.FreezeHeaderRow, value); }
}
public bool BoldHeaderRow
{
get { return this.GetOptionValue<bool>(SerializationOptionsHelper.BoldHeaderRow); }
set { this.SetOptionValue<bool>(SerializationOptionsHelper.BoldHeaderRow, value); }
}
public bool AutoFilterHeaderRow
{
get { return this.GetOptionValue<bool>(SerializationOptionsHelper.AutoFilterHeaderRow); }
set { this.SetOptionValue<bool>(SerializationOptionsHelper.AutoFilterHeaderRow, value); }
}
public bool AutoSizeColumns
{
get { return this.GetOptionValue<bool>(SerializationOptionsHelper.AutoSizeColumns); }
set { this.SetOptionValue<bool>(SerializationOptionsHelper.AutoSizeColumns, value); }
}
} }
public class SerializeDataResult public class SerializeDataResult
@@ -184,5 +208,9 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts
internal const string Encoding = "encoding"; internal const string Encoding = "encoding";
internal const string Formatted = "formatted"; internal const string Formatted = "formatted";
internal const string MaxCharsToStore = "maxchars"; internal const string MaxCharsToStore = "maxchars";
internal const string FreezeHeaderRow = "freezeHeaderRow";
internal const string BoldHeaderRow = "boldHeaderRow";
internal const string AutoFilterHeaderRow = "autoFilterHeaderRow";
internal const string AutoSizeColumns = "autoSizeColumns";
} }
} }

View File

@@ -5,9 +5,11 @@
#nullable disable #nullable disable
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
using SkiaSharp;
namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
{ {
@@ -16,14 +18,28 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
/// </summary> /// </summary>
public class SaveAsExcelFileStreamWriter : SaveAsStreamWriter public class SaveAsExcelFileStreamWriter : SaveAsStreamWriter
{ {
// Font family used in Excel sheet
private const string FontFamily = "Calibri";
// Font size in Excel sheet (points with conversion to pixels)
private const float FontSizePixels = 11F * (96F / 72F);
// Pixel width of auto-filter button
private const float AutoFilterPixelWidth = 17F;
#region Member Variables #region Member Variables
private readonly SaveResultsAsExcelRequestParams saveParams; private readonly SaveResultsAsExcelRequestParams saveParams;
private readonly float[] columnWidths;
private readonly int columnEndIndex;
private readonly int columnStartIndex;
private readonly SaveAsExcelFileStreamWriterHelper helper;
private bool headerWritten; private bool headerWritten;
private SaveAsExcelFileStreamWriterHelper helper;
private SaveAsExcelFileStreamWriterHelper.ExcelSheet sheet; private SaveAsExcelFileStreamWriterHelper.ExcelSheet sheet;
private SKPaint paint;
#endregion #endregion
/// <summary> /// <summary>
@@ -41,7 +57,89 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
{ {
saveParams = requestParams; saveParams = requestParams;
helper = new SaveAsExcelFileStreamWriterHelper(stream); helper = new SaveAsExcelFileStreamWriterHelper(stream);
sheet = helper.AddSheet();
// Do some setup if the caller requested automatically sized columns
if (requestParams.AutoSizeColumns)
{
// Set column counts depending on whether save request is for entire set or a subset
columnEndIndex = columns.Count;
columnStartIndex = 0;
var columnCount = columns.Count;
if (requestParams.IsSaveSelection)
{
// ReSharper disable PossibleInvalidOperationException IsSaveSelection verifies these values exist
columnEndIndex = requestParams.ColumnEndIndex.Value + 1;
columnStartIndex = requestParams.ColumnStartIndex.Value;
columnCount = columnEndIndex - columnStartIndex;
// ReSharper restore PossibleInvalidOperationException
}
columnWidths = new float[columnCount];
// If the caller requested headers the column widths can be initially set based on the header values
if (requestParams.IncludeHeaders)
{
// Setup for measuring the header, set font style based on whether the header should be bold or not
using var headerPaint = new SKPaint();
headerPaint.Typeface = SKTypeface.FromFamilyName(FontFamily, requestParams.BoldHeaderRow ? SKFontStyle.Bold : SKFontStyle.Normal);
headerPaint.TextSize = FontSizePixels;
var skBounds = SKRect.Empty;
// Loop over all the columns
for (int columnIndex = columnStartIndex; columnIndex < columnEndIndex; ++columnIndex)
{
var columnNumber = columnIndex - columnStartIndex;
// Measure the header text
var textWidth = headerPaint.MeasureText(columns[columnIndex].ColumnName.AsSpan(), ref skBounds);
// Add extra for the auto filter button if requested
if (requestParams.AutoFilterHeaderRow)
{
textWidth += AutoFilterPixelWidth;
}
// Just store the width as a starting point
columnWidths[columnNumber] = textWidth;
}
}
}
}
/// <summary>
/// Excel supports specifying column widths so measure if the user wants the columns automatically sized
/// </summary>
public override bool ShouldMeasureRowColumns => saveParams.AutoSizeColumns;
/// <summary>
/// Measures each column of a row of data and stores updates the maximum width of the column if needed
/// </summary>
/// <param name="row">The row of data to measure</param>
public override void MeasureRowColumns(IList<DbCellValue> row)
{
// Create the paint object if not done already
if (paint == null)
{
paint = new SKPaint();
paint.Typeface = SKTypeface.FromFamilyName(FontFamily);
paint.TextSize = FontSizePixels;
}
var skBounds = SKRect.Empty;
// Loop over all the columns
for (int columnIndex = columnStartIndex; columnIndex < columnEndIndex; ++columnIndex)
{
var columnNumber = columnIndex - columnStartIndex;
// Measure the width of the text
var textWidth = paint.MeasureText(row[columnIndex].DisplayValue.AsSpan(), ref skBounds);
// Update the max if the new width is greater
columnWidths[columnNumber] = Math.Max(columnWidths[columnNumber], textWidth);
}
} }
/// <summary> /// <summary>
@@ -55,13 +153,44 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
/// </param> /// </param>
public override void WriteRow(IList<DbCellValue> row, IReadOnlyList<DbColumnWrapper> columns) public override void WriteRow(IList<DbCellValue> row, IReadOnlyList<DbColumnWrapper> columns)
{ {
// Check to make sure the sheet has been created
if (sheet == null)
{
// Get rid of any paint object from the auto-sizing
paint?.Dispose();
// Create the blank sheet
sheet = helper.AddSheet(null, columns.Count);
// The XLSX format has strict ordering requirements so these must be done in the proper order
// First freeze the header row if the caller has requested header rows and that the header should be frozen
if (saveParams.IncludeHeaders && saveParams.FreezeHeaderRow)
{
sheet.FreezeHeaderRow();
}
// Next if column widths have been specified they should be saved to the sheet
if (columnWidths != null)
{
sheet.WriteColumnInformation(columnWidths);
}
// Lastly enable auto filter if the caller has requested header rows and that the header should be frozen
if (saveParams.IncludeHeaders && saveParams.AutoFilterHeaderRow)
{
sheet.EnableAutoFilter();
}
}
// Write out the header if we haven't already and the user chose to have it // Write out the header if we haven't already and the user chose to have it
if (saveParams.IncludeHeaders && !headerWritten) if (saveParams.IncludeHeaders && !headerWritten)
{ {
sheet.AddRow(); sheet.AddRow();
for (int i = ColumnStartIndex; i <= ColumnEndIndex; i++) for (int i = ColumnStartIndex; i <= ColumnEndIndex; i++)
{ {
sheet.AddCell(columns[i].ColumnName); // Add the header text and bold if requested
sheet.AddCell(columns[i].ColumnName, saveParams.BoldHeaderRow);
} }
headerWritten = true; headerWritten = true;
} }
@@ -74,7 +203,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
} }
private bool disposed; private bool disposed;
override protected void Dispose(bool disposing) protected override void Dispose(bool disposing)
{ {
if (disposed) if (disposed)
return; return;

View File

@@ -6,6 +6,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Data.SqlTypes; using System.Data.SqlTypes;
using System.Globalization;
using System.IO; using System.IO;
using System.IO.Compression; using System.IO.Compression;
using System.Xml; using System.Xml;
@@ -75,21 +76,31 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
// new TimeSpan(24,0,0).Ticks // new TimeSpan(24,0,0).Ticks
private const long TicksPerDay = 864000000000L; private const long TicksPerDay = 864000000000L;
// Digit pixel width for 11 point Calibri
private const float FontPixelWidth = 7;
private XmlWriter writer; private XmlWriter writer;
private ReferenceManager referenceManager; private ReferenceManager referenceManager;
private bool hasOpenRowTag; private bool hasOpenRowTag;
private readonly int columnCount;
private bool autoFilterColumns;
private bool hasStartedSheetData;
/// <summary> /// <summary>
/// Initializes a new instance of the ExcelSheet class. /// Initializes a new instance of the ExcelSheet class.
/// </summary> /// </summary>
/// <param name="writer">XmlWriter to write the sheet data</param> /// <param name="writer">XmlWriter to write the sheet data</param>
internal ExcelSheet(XmlWriter writer) /// <param name="columnCount">Number of columns in the new sheet</param>
internal ExcelSheet(XmlWriter writer, int columnCount)
{ {
this.writer = writer; this.writer = writer;
this.columnCount = columnCount;
writer.WriteStartDocument(); writer.WriteStartDocument();
writer.WriteStartElement("worksheet", "http://schemas.openxmlformats.org/spreadsheetml/2006/main"); writer.WriteStartElement("worksheet", "http://schemas.openxmlformats.org/spreadsheetml/2006/main");
writer.WriteAttributeString("xmlns", "r", null, "http://schemas.openxmlformats.org/officeDocument/2006/relationships"); writer.WriteAttributeString("xmlns", "r", null, "http://schemas.openxmlformats.org/officeDocument/2006/relationships");
writer.WriteStartElement("sheetData");
referenceManager = new ReferenceManager(writer); referenceManager = new ReferenceManager(writer);
} }
@@ -98,6 +109,13 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
/// </summary> /// </summary>
public void AddRow() public void AddRow()
{ {
// Write the open tag for sheetData if it hasn't been written yet
if (!hasStartedSheetData)
{
writer.WriteStartElement("sheetData");
hasStartedSheetData = true;
}
EndRowIfNeeded(); EndRowIfNeeded();
hasOpenRowTag = true; hasOpenRowTag = true;
@@ -110,8 +128,9 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
/// <summary> /// <summary>
/// Write a string cell /// Write a string cell
/// </summary> /// </summary>
/// <param name="value">string value to write</param> /// <param name="value">String value to write</param>
public void AddCell(string value) /// <param name="bold">Whether the cell should be bold, defaults to false</param>
public void AddCell(string value, bool bold = false)
{ {
// string needs <c t="inlineStr"><is><t>string</t></is></c> // string needs <c t="inlineStr"><is><t>string</t></is></c>
// This class uses inlineStr instead of more common shared string table // This class uses inlineStr instead of more common shared string table
@@ -129,21 +148,28 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
writer.WriteAttributeString("t", "inlineStr"); writer.WriteAttributeString("t", "inlineStr");
// Write the style attribute set to the bold font if requested
if (bold)
{
writer.WriteAttributeString("s", "5");
}
writer.WriteStartElement("is"); writer.WriteStartElement("is");
writer.WriteStartElement("t"); writer.WriteStartElement("t");
writer.WriteValue(value); writer.WriteValue(value);
writer.WriteEndElement(); writer.WriteEndElement(); // <t>
writer.WriteEndElement(); writer.WriteEndElement(); // <is>
writer.WriteEndElement(); writer.WriteEndElement(); // <c>
} }
/// <summary> /// <summary>
/// Write a object cell /// Write a object cell
/// </summary> /// </summary>
/// The program will try to output number/datetime, otherwise, call the ToString /// The program will try to output number/datetime, otherwise, call the ToString
/// <param name="o"></param> /// <param name="dbCellValue">DbCellValue to write based on data type</param>
public void AddCell(DbCellValue dbCellValue) /// <param name="bold">Whether the cell should be bold, defaults to false</param>
public void AddCell(DbCellValue dbCellValue, bool bold = false)
{ {
object o = dbCellValue.RawObject; object o = dbCellValue.RawObject;
if (dbCellValue.IsNull || o == null) if (dbCellValue.IsNull || o == null)
@@ -169,26 +195,101 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
AddCell((DateTime)o); AddCell((DateTime)o);
break; break;
case TypeCode.String: case TypeCode.String:
AddCell((string)o); AddCell((string)o, bold);
break; break;
default: default:
if (o is TimeSpan span) //TimeSpan doesn't have TypeCode if (o is TimeSpan span) //TimeSpan doesn't have TypeCode
{ {
AddCell(span); AddCell(span);
} }
// We need to handle SqlDecimal and SqlMoney types here because we can't convert them to .NET types due to different precisons in SQL Server and .NET. // We need to handle SqlDecimal and SqlMoney types here because we can't convert them to .NET types due to different precisions in SQL Server and .NET.
else if (o is SqlDecimal || o is SqlMoney) else if (o is SqlDecimal || o is SqlMoney)
{ {
AddCellBoxedNumber(dbCellValue.DisplayValue); AddCellBoxedNumber(dbCellValue.DisplayValue);
} }
else else
{ {
AddCell(dbCellValue.DisplayValue); AddCell(dbCellValue.DisplayValue, bold);
} }
break; break;
} }
} }
/// <summary>
/// Write a sheetView that freezes the top row. Must be called before any rows have been added.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown if called after any rows have been added.</exception>
public void FreezeHeaderRow()
{
if (hasStartedSheetData)
{
throw new InvalidOperationException("Must be called before calling AddRow");
}
writer.WriteStartElement("sheetViews");
writer.WriteStartElement("sheetView");
writer.WriteAttributeString("tabSelected", "1");
writer.WriteAttributeString("workbookViewId", "0");
writer.WriteStartElement("pane");
writer.WriteAttributeString("ySplit", "1");
writer.WriteAttributeString("topLeftCell", "A2");
writer.WriteAttributeString("activePane", "bottomLeft");
writer.WriteAttributeString("state", "frozen");
writer.WriteEndElement(); // <pane>
writer.WriteStartElement("selection");
writer.WriteAttributeString("pane", "bottomLeft");
writer.WriteEndElement(); // <selection>
writer.WriteEndElement(); // <sheetView>
writer.WriteEndElement(); // <sheetViews>
}
/// <summary>
/// Enable auto filtering, the XML will be written when closing the sheet later
/// </summary>
public void EnableAutoFilter()
{
autoFilterColumns = true;
}
/// <summary>
/// Write the columns widths. Must be called before any rows have been added.
/// </summary>
/// <param name="columnWidths">Array with the widths of each column.</param>
/// <exception cref="InvalidOperationException">Thrown if called after any rows have been added.</exception>
public void WriteColumnInformation(float[] columnWidths)
{
if (hasStartedSheetData)
{
throw new InvalidOperationException("Must be called before calling AddRow");
}
if (columnWidths.Length != this.columnCount)
{
throw new InvalidOperationException("Column count mismatch");
}
writer.WriteStartElement("cols");
for (int columnIndex = 0; columnIndex < columnWidths.Length; columnIndex++)
{
var columnWidth = Math.Truncate((columnWidths[columnIndex] + 15) / FontPixelWidth * 256) / 256;
writer.WriteStartElement("col");
writer.WriteAttributeString("min", (columnIndex + 1).ToString());
writer.WriteAttributeString("max", (columnIndex + 1).ToString());
writer.WriteAttributeString("width", columnWidth.ToString(CultureInfo.InvariantCulture));
writer.WriteAttributeString("bestFit", "1");
writer.WriteAttributeString("customWidth", "1");
writer.WriteEndElement(); // <col>
}
writer.WriteEndElement(); // <cols>
}
/// <summary> /// <summary>
/// Close the <row><sheetData><worksheet> tags and close the stream /// Close the <row><sheetData><worksheet> tags and close the stream
/// </summary> /// </summary>
@@ -196,6 +297,15 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
{ {
EndRowIfNeeded(); EndRowIfNeeded();
writer.WriteEndElement(); // <sheetData> writer.WriteEndElement(); // <sheetData>
// Write the auto filter XML if requested
if (autoFilterColumns)
{
writer.WriteStartElement("autoFilter");
writer.WriteAttributeString("ref", $"A1:{ReferenceManager.GetColumnName(this.columnCount)}1");
writer.WriteEndElement(); // <autoFilter>
}
writer.WriteEndElement(); // <worksheet> writer.WriteEndElement(); // <worksheet>
writer.Dispose(); writer.Dispose();
} }
@@ -203,7 +313,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
/// <summary> /// <summary>
/// Write a empty cell /// Write a empty cell
/// </summary> /// </summary>
/// This only increases the internal bookmark and doesn't arcturally write out anything. /// This only increases the internal bookmark and doesn't actually write out anything.
private void AddCellEmpty() private void AddCellEmpty()
{ {
referenceManager.IncreaseColumnReference(); referenceManager.IncreaseColumnReference();
@@ -212,7 +322,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
/// <summary> /// <summary>
/// Write a bool cell. /// Write a bool cell.
/// </summary> /// </summary>
/// <param name="time"></param> /// <param name="value">Boolean value to write</param>
private void AddCell(bool value) private void AddCell(bool value)
{ {
// Excel FALSE: <c r="A1" t="b"><v>0</v></c> // Excel FALSE: <c r="A1" t="b"><v>0</v></c>
@@ -234,15 +344,15 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
{ {
writer.WriteValue("0"); writer.WriteValue("0");
} }
writer.WriteEndElement(); writer.WriteEndElement(); // <v>
writer.WriteEndElement(); writer.WriteEndElement(); // <c>
} }
/// <summary> /// <summary>
/// Write a TimeSpan cell. /// Write a TimeSpan cell.
/// </summary> /// </summary>
/// <param name="time"></param> /// <param name="time">TimeSpan value to write</param>
private void AddCell(TimeSpan time) private void AddCell(TimeSpan time)
{ {
referenceManager.AssureColumnReference(); referenceManager.AssureColumnReference();
@@ -262,7 +372,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
/// <summary> /// <summary>
/// Write a DateTime cell. /// Write a DateTime cell.
/// </summary> /// </summary>
/// <param name="dateTime">Datetime</param> /// <param name="dateTime">DateTime value to write</param>
/// <remark> /// <remark>
/// If the DateTime does not have date part, it will be written as datetime and show as time only /// If the DateTime does not have date part, it will be written as datetime and show as time only
/// If the DateTime is before 1900-03-01, save as string because excel doesn't support them. /// If the DateTime is before 1900-03-01, save as string because excel doesn't support them.
@@ -307,9 +417,9 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
writer.WriteStartElement("v"); writer.WriteStartElement("v");
writer.WriteValue(number); writer.WriteValue(number);
writer.WriteEndElement(); writer.WriteEndElement(); // <v>
writer.WriteEndElement(); writer.WriteEndElement(); // <c>
} }
@@ -326,9 +436,9 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
writer.WriteStartElement("v"); writer.WriteStartElement("v");
writer.WriteValue(excelDate); writer.WriteValue(excelDate);
writer.WriteEndElement(); writer.WriteEndElement(); // <v>
writer.WriteEndElement(); writer.WriteEndElement(); // <c>
} }
private void EndRowIfNeeded() private void EndRowIfNeeded()
@@ -338,8 +448,6 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
writer.WriteEndElement(); // <row> writer.WriteEndElement(); // <row>
} }
} }
} }
/// <summary> /// <summary>
@@ -429,6 +537,26 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
} }
} }
/// <summary>
/// Gets the column name (letters) from a column number
/// https://stackoverflow.com/questions/181596/how-to-convert-a-column-number-e-g-127-into-an-excel-column-e-g-aa
/// </summary>
/// <param name="columnNumber">The column number.</param>
/// <returns>The column name.</returns>
public static string GetColumnName(int columnNumber)
{
string columnName = "";
while (columnNumber > 0)
{
int modulo = (columnNumber - 1) % 26;
columnName = Convert.ToChar('A' + modulo) + columnName;
columnNumber = (columnNumber - modulo) / 26;
}
return columnName;
}
/// <summary> /// <summary>
/// Check that we have not write too many rows. (xlsx has a limit of 1048576 rows) /// Check that we have not write too many rows. (xlsx has a limit of 1048576 rows)
/// </summary> /// </summary>
@@ -511,11 +639,11 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
/// <param name="sheetName">Sheet name</param> /// <param name="sheetName">Sheet name</param>
/// <returns>ExcelSheet for writing the sheet content</returns> /// <returns>ExcelSheet for writing the sheet content</returns>
/// <remarks> /// <remarks>
/// When the sheetName is null, sheet1,shhet2,..., will be used. /// When the sheetName is null, sheet1,sheet2,..., will be used.
/// The following charactors are not allowed in the sheetName /// The following characters are not allowed in the sheetName
/// '\', '/','*','[',']',':','?' /// '\', '/','*','[',']',':','?'
/// </remarks> /// </remarks>
public ExcelSheet AddSheet(string sheetName = null) public ExcelSheet AddSheet(string sheetName, int columnCount)
{ {
string sheetFileName = "sheet" + (sheetNames.Count + 1); string sheetFileName = "sheet" + (sheetNames.Count + 1);
sheetName ??= sheetFileName; sheetName ??= sheetFileName;
@@ -523,7 +651,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
sheetNames.Add(sheetName); sheetNames.Add(sheetName);
XmlWriter sheetWriter = AddEntry($"xl/worksheets/{sheetFileName}.xml"); XmlWriter sheetWriter = AddEntry($"xl/worksheets/{sheetFileName}.xml");
return new ExcelSheet(sheetWriter); return new ExcelSheet(sheetWriter, columnCount);
} }
/// <summary> /// <summary>
@@ -568,26 +696,26 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
xw.WriteStartElement("Default"); xw.WriteStartElement("Default");
xw.WriteAttributeString("Extension", "rels"); xw.WriteAttributeString("Extension", "rels");
xw.WriteAttributeString("ContentType", "application/vnd.openxmlformats-package.relationships+xml"); xw.WriteAttributeString("ContentType", "application/vnd.openxmlformats-package.relationships+xml");
xw.WriteEndElement(); xw.WriteEndElement(); // <Default>
xw.WriteStartElement("Override"); xw.WriteStartElement("Override");
xw.WriteAttributeString("PartName", "/xl/workbook.xml"); xw.WriteAttributeString("PartName", "/xl/workbook.xml");
xw.WriteAttributeString("ContentType", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"); xw.WriteAttributeString("ContentType", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml");
xw.WriteEndElement(); xw.WriteEndElement(); // <Override>
xw.WriteStartElement("Override"); xw.WriteStartElement("Override");
xw.WriteAttributeString("PartName", "/xl/styles.xml"); xw.WriteAttributeString("PartName", "/xl/styles.xml");
xw.WriteAttributeString("ContentType", "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"); xw.WriteAttributeString("ContentType", "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml");
xw.WriteEndElement(); xw.WriteEndElement(); // <Override>
for (int i = 1; i <= sheetNames.Count; ++i) for (int i = 1; i <= sheetNames.Count; ++i)
{ {
xw.WriteStartElement("Override"); xw.WriteStartElement("Override");
xw.WriteAttributeString("PartName", "/xl/worksheets/sheet" + i + ".xml"); xw.WriteAttributeString("PartName", "/xl/worksheets/sheet" + i + ".xml");
xw.WriteAttributeString("ContentType", "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"); xw.WriteAttributeString("ContentType", "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml");
xw.WriteEndElement(); xw.WriteEndElement(); // <Override>
} }
xw.WriteEndElement(); xw.WriteEndElement(); // <Types>
xw.WriteEndDocument(); xw.WriteEndDocument();
} }
} }
@@ -607,9 +735,9 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
xw.WriteAttributeString("Id", "rId1"); xw.WriteAttributeString("Id", "rId1");
xw.WriteAttributeString("Type", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument"); xw.WriteAttributeString("Type", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument");
xw.WriteAttributeString("Target", "xl/workbook.xml"); xw.WriteAttributeString("Target", "xl/workbook.xml");
xw.WriteEndElement(); xw.WriteEndElement(); // <Relationship>
xw.WriteEndElement(); xw.WriteEndElement(); // <Relationships>
xw.WriteEndDocument(); xw.WriteEndDocument();
} }
@@ -648,7 +776,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
xw.WriteAttributeString("name", sheetNames[i - 1]); xw.WriteAttributeString("name", sheetNames[i - 1]);
xw.WriteAttributeString("sheetId", i.ToString()); xw.WriteAttributeString("sheetId", i.ToString());
xw.WriteAttributeString("r", "id", null, "rId" + i); xw.WriteAttributeString("r", "id", null, "rId" + i);
xw.WriteEndElement(); xw.WriteEndElement(); // <sheet>
} }
xw.WriteEndDocument(); xw.WriteEndDocument();
} }
@@ -668,7 +796,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
xw.WriteAttributeString("Id", "rId0"); xw.WriteAttributeString("Id", "rId0");
xw.WriteAttributeString("Type", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles"); xw.WriteAttributeString("Type", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles");
xw.WriteAttributeString("Target", "styles.xml"); xw.WriteAttributeString("Target", "styles.xml");
xw.WriteEndElement(); xw.WriteEndElement(); // <Relationship>
for (int i = 1; i <= sheetNames.Count; i++) for (int i = 1; i <= sheetNames.Count; i++)
{ {
@@ -676,9 +804,9 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
xw.WriteAttributeString("Id", "rId" + i); xw.WriteAttributeString("Id", "rId" + i);
xw.WriteAttributeString("Type", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"); xw.WriteAttributeString("Type", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet");
xw.WriteAttributeString("Target", "worksheets/sheet" + i + ".xml"); xw.WriteAttributeString("Target", "worksheets/sheet" + i + ".xml");
xw.WriteEndElement(); xw.WriteEndElement(); // <Relationship>
} }
xw.WriteEndElement(); xw.WriteEndElement(); // <Relationships>
xw.WriteEndDocument(); xw.WriteEndDocument();
} }
} }
@@ -702,41 +830,63 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
xw.WriteStartElement("numFmt"); xw.WriteStartElement("numFmt");
xw.WriteAttributeString("numFmtId", "166"); xw.WriteAttributeString("numFmtId", "166");
xw.WriteAttributeString("formatCode", "yyyy-mm-dd"); xw.WriteAttributeString("formatCode", "yyyy-mm-dd");
xw.WriteEndElement(); xw.WriteEndElement(); // <numFmt>
xw.WriteStartElement("numFmt"); xw.WriteStartElement("numFmt");
xw.WriteAttributeString("numFmtId", "167"); xw.WriteAttributeString("numFmtId", "167");
xw.WriteAttributeString("formatCode", "hh:mm:ss"); xw.WriteAttributeString("formatCode", "hh:mm:ss");
xw.WriteEndElement(); xw.WriteEndElement(); // <numFmt>
xw.WriteStartElement("numFmt"); xw.WriteStartElement("numFmt");
xw.WriteAttributeString("numFmtId", "168"); xw.WriteAttributeString("numFmtId", "168");
xw.WriteAttributeString("formatCode", "yyyy-mm-dd hh:mm:ss"); xw.WriteAttributeString("formatCode", "yyyy-mm-dd hh:mm:ss");
xw.WriteEndElement(); xw.WriteEndElement(); // <numFmt>
xw.WriteStartElement("numFmt"); xw.WriteStartElement("numFmt");
xw.WriteAttributeString("numFmtId", "169"); xw.WriteAttributeString("numFmtId", "169");
xw.WriteAttributeString("formatCode", "[h]:mm:ss"); xw.WriteAttributeString("formatCode", "[h]:mm:ss");
xw.WriteEndElement(); xw.WriteEndElement(); // <numFmt>
xw.WriteEndElement(); //mumFmts xw.WriteEndElement(); // <numFmts>
xw.WriteStartElement("fonts"); xw.WriteStartElement("fonts");
xw.WriteAttributeString("count", "1"); xw.WriteAttributeString("count", "2");
xw.WriteStartElement("font"); xw.WriteStartElement("font");
xw.WriteStartElement("sz"); xw.WriteStartElement("sz");
xw.WriteAttributeString("val", "11"); xw.WriteAttributeString("val", "11");
xw.WriteEndElement(); xw.WriteEndElement(); // <sz>
xw.WriteStartElement("color"); xw.WriteStartElement("color");
xw.WriteAttributeString("theme", "1"); xw.WriteAttributeString("theme", "1");
xw.WriteEndElement(); xw.WriteEndElement(); // <color>
xw.WriteStartElement("name"); xw.WriteStartElement("name");
xw.WriteAttributeString("val", "Calibri"); xw.WriteAttributeString("val", "Calibri");
xw.WriteEndElement(); xw.WriteEndElement(); // <name>
xw.WriteStartElement("family"); xw.WriteStartElement("family");
xw.WriteAttributeString("val", "2"); xw.WriteAttributeString("val", "2");
xw.WriteEndElement(); xw.WriteEndElement(); // <family>
xw.WriteStartElement("scheme"); xw.WriteStartElement("scheme");
xw.WriteAttributeString("val", "minor"); xw.WriteAttributeString("val", "minor");
xw.WriteEndElement(); xw.WriteEndElement(); // <scheme>
xw.WriteEndElement(); // font xw.WriteEndElement(); // <font>
xw.WriteStartElement("font");
xw.WriteStartElement("b");
xw.WriteEndElement(); // <b>
xw.WriteStartElement("sz");
xw.WriteAttributeString("val", "11");
xw.WriteEndElement(); // <sz>
xw.WriteStartElement("color");
xw.WriteAttributeString("theme", "1");
xw.WriteEndElement(); // <color>
xw.WriteStartElement("name");
xw.WriteAttributeString("val", "Calibri");
xw.WriteEndElement(); // <name>
xw.WriteStartElement("family");
xw.WriteAttributeString("val", "2");
xw.WriteEndElement(); // <family>
xw.WriteStartElement("scheme");
xw.WriteAttributeString("val", "minor");
xw.WriteEndElement(); // <scheme>
xw.WriteEndElement(); // <font>
xw.WriteEndElement(); // fonts xw.WriteEndElement(); // fonts
xw.WriteStartElement("fills"); xw.WriteStartElement("fills");
@@ -744,9 +894,9 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
xw.WriteStartElement("fill"); xw.WriteStartElement("fill");
xw.WriteStartElement("patternFill"); xw.WriteStartElement("patternFill");
xw.WriteAttributeString("patternType", "none"); xw.WriteAttributeString("patternType", "none");
xw.WriteEndElement(); // patternFill xw.WriteEndElement(); // <patternFill>
xw.WriteEndElement(); // fill xw.WriteEndElement(); // <fill>
xw.WriteEndElement(); // fills xw.WriteEndElement(); // <fills>
xw.WriteStartElement("borders"); xw.WriteStartElement("borders");
xw.WriteAttributeString("count", "1"); xw.WriteAttributeString("count", "1");
@@ -756,8 +906,8 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
xw.WriteElementString("top", null); xw.WriteElementString("top", null);
xw.WriteElementString("bottom", null); xw.WriteElementString("bottom", null);
xw.WriteElementString("diagonal", null); xw.WriteElementString("diagonal", null);
xw.WriteEndElement(); // board xw.WriteEndElement(); // <border>
xw.WriteEndElement(); // borders xw.WriteEndElement(); // <borders>
xw.WriteStartElement("cellStyleXfs"); xw.WriteStartElement("cellStyleXfs");
xw.WriteAttributeString("count", "1"); xw.WriteAttributeString("count", "1");
@@ -766,35 +916,39 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
xw.WriteAttributeString("fontId", "0"); xw.WriteAttributeString("fontId", "0");
xw.WriteAttributeString("fillId", "0"); xw.WriteAttributeString("fillId", "0");
xw.WriteAttributeString("borderId", "0"); xw.WriteAttributeString("borderId", "0");
xw.WriteEndElement(); // xf xw.WriteEndElement(); // <xf>
xw.WriteEndElement(); // cellStyleXfs xw.WriteEndElement(); // <cellStyleXfs>
xw.WriteStartElement("cellXfs"); xw.WriteStartElement("cellXfs");
xw.WriteAttributeString("count", "5"); xw.WriteAttributeString("count", "6");
xw.WriteStartElement("xf"); xw.WriteStartElement("xf");
xw.WriteAttributeString("xfId", "0"); xw.WriteAttributeString("xfId", "0");
xw.WriteEndElement(); xw.WriteEndElement(); // <xf>
xw.WriteStartElement("xf"); xw.WriteStartElement("xf");
xw.WriteAttributeString("numFmtId", "166"); xw.WriteAttributeString("numFmtId", "166");
xw.WriteAttributeString("xfId", "0"); xw.WriteAttributeString("xfId", "0");
xw.WriteAttributeString("applyNumberFormat", "1"); xw.WriteAttributeString("applyNumberFormat", "1");
xw.WriteEndElement(); xw.WriteEndElement(); // <xf>
xw.WriteStartElement("xf"); xw.WriteStartElement("xf");
xw.WriteAttributeString("numFmtId", "167"); xw.WriteAttributeString("numFmtId", "167");
xw.WriteAttributeString("xfId", "0"); xw.WriteAttributeString("xfId", "0");
xw.WriteAttributeString("applyNumberFormat", "1"); xw.WriteAttributeString("applyNumberFormat", "1");
xw.WriteEndElement(); xw.WriteEndElement(); // <xf>
xw.WriteStartElement("xf"); xw.WriteStartElement("xf");
xw.WriteAttributeString("numFmtId", "168"); xw.WriteAttributeString("numFmtId", "168");
xw.WriteAttributeString("xfId", "0"); xw.WriteAttributeString("xfId", "0");
xw.WriteAttributeString("applyNumberFormat", "1"); xw.WriteAttributeString("applyNumberFormat", "1");
xw.WriteEndElement(); xw.WriteEndElement(); // <xf>
xw.WriteStartElement("xf"); xw.WriteStartElement("xf");
xw.WriteAttributeString("numFmtId", "169"); xw.WriteAttributeString("numFmtId", "169");
xw.WriteAttributeString("xfId", "0"); xw.WriteAttributeString("xfId", "0");
xw.WriteAttributeString("applyNumberFormat", "1"); xw.WriteAttributeString("applyNumberFormat", "1");
xw.WriteEndElement(); xw.WriteEndElement(); // <xf>
xw.WriteEndElement(); // cellXfs xw.WriteStartElement("xf");
xw.WriteAttributeString("xfId", "0");
xw.WriteAttributeString("fontId", "1");
xw.WriteEndElement(); // <xf>
xw.WriteEndElement(); // <cellXfs>
xw.WriteStartElement("cellStyles"); xw.WriteStartElement("cellStyles");
xw.WriteAttributeString("count", "1"); xw.WriteAttributeString("count", "1");
@@ -802,8 +956,8 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
xw.WriteAttributeString("name", "Normal"); xw.WriteAttributeString("name", "Normal");
xw.WriteAttributeString("builtinId", "0"); xw.WriteAttributeString("builtinId", "0");
xw.WriteAttributeString("xfId", "0"); xw.WriteAttributeString("xfId", "0");
xw.WriteEndElement(); // cellStyle xw.WriteEndElement(); // <cellStyle>
xw.WriteEndElement(); // cellStyles xw.WriteEndElement(); // <cellStyles>
} }
} }
} }

View File

@@ -85,6 +85,17 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
throw new InvalidOperationException("This type of writer is meant to write values from a list of cell values only."); throw new InvalidOperationException("This type of writer is meant to write values from a list of cell values only.");
} }
/// <summary>
/// Indicates whether columns should be measured based on whether the output format supports it and if the caller wants the columns automatically sized
/// </summary>
public virtual bool ShouldMeasureRowColumns => false;
/// <summary>
/// Measures the columns in a row of data as part of determining automatic column widths
/// </summary>
/// <param name="row">The row of data to measure</param>
public virtual void MeasureRowColumns(IList<DbCellValue> row) { }
/// <summary> /// <summary>
/// Writes a row of data to the output file using the format provided by the implementing class. /// Writes a row of data to the output file using the format provided by the implementing class.
/// </summary> /// </summary>

View File

@@ -533,8 +533,30 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
using (var fileReader = fileFactory.GetReader(outputFileName)) using (var fileReader = fileFactory.GetReader(outputFileName))
using (var fileWriter = fileFactory.GetWriter(saveParams.FilePath, Columns)) using (var fileWriter = fileFactory.GetWriter(saveParams.FilePath, Columns))
{ {
DateTime recentLogTime;
// Some writers (like Excel) require the column widths before any of the rows have been written so we need to loop over
// all the rows of data in order to calculate what the column widths should be before the rows are actually written
if (fileWriter is SaveAsStreamWriter { ShouldMeasureRowColumns: true } saveAsStreamWriter)
{
Logger.Verbose($"Started measuring {RowCount} rows");
recentLogTime = DateTime.Now;
// Iterate over the rows that are in the selected row set
for (long i = rowStartIndex; i < rowEndIndex; ++i)
{
var row = fileReader.ReadRow(fileOffsets[i], i, Columns);
saveAsStreamWriter.MeasureRowColumns(row);
if (DateTime.Now.Subtract(recentLogTime).TotalSeconds > 1)
{
Logger.Verbose($"Measure progress: {i - rowStartIndex + 1}/{RowCount}.");
recentLogTime = DateTime.Now;
}
}
Logger.Verbose($"Measured {RowCount} rows");
}
Logger.Verbose($"Started exporting {RowCount} rows to file: {outputFileName}"); Logger.Verbose($"Started exporting {RowCount} rows to file: {outputFileName}");
var recentLogTime = DateTime.Now; recentLogTime = DateTime.Now;
// Iterate over the rows that are in the selected row set // Iterate over the rows that are in the selected row set
for (long i = rowStartIndex; i < rowEndIndex; ++i) for (long i = rowStartIndex; i < rowEndIndex; ++i)
{ {

View File

@@ -290,7 +290,11 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
FilePath = this.requestParams.FilePath, FilePath = this.requestParams.FilePath,
BatchIndex = 0, BatchIndex = 0,
ResultSetIndex = 0, ResultSetIndex = 0,
IncludeHeaders = this.requestParams.IncludeHeaders IncludeHeaders = this.requestParams.IncludeHeaders,
FreezeHeaderRow = this.requestParams.FreezeHeaderRow,
BoldHeaderRow = this.requestParams.BoldHeaderRow,
AutoFilterHeaderRow = this.requestParams.AutoFilterHeaderRow,
AutoSizeColumns = this.requestParams.AutoSizeColumns
}; };
} }
private SaveResultsAsCsvRequestParams CreateCsvRequestParams() private SaveResultsAsCsvRequestParams CreateCsvRequestParams()

View File

@@ -6,7 +6,7 @@
<numFmt numFmtId="168" formatCode="yyyy-mm-dd hh:mm:ss" /> <numFmt numFmtId="168" formatCode="yyyy-mm-dd hh:mm:ss" />
<numFmt numFmtId="169" formatCode="[h]:mm:ss" /> <numFmt numFmtId="169" formatCode="[h]:mm:ss" />
</numFmts> </numFmts>
<fonts count="1"> <fonts count="2">
<font> <font>
<sz val="11" /> <sz val="11" />
<color theme="1" /> <color theme="1" />
@@ -14,6 +14,14 @@
<family val="2" /> <family val="2" />
<scheme val="minor" /> <scheme val="minor" />
</font> </font>
<font>
<b />
<sz val="11" />
<color theme="1" />
<name val="Calibri" />
<family val="2" />
<scheme val="minor" />
</font>
</fonts> </fonts>
<fills count="1"> <fills count="1">
<fill> <fill>
@@ -32,12 +40,13 @@
<cellStyleXfs count="1"> <cellStyleXfs count="1">
<xf numFmtId="0" fontId="0" fillId="0" borderId="0" /> <xf numFmtId="0" fontId="0" fillId="0" borderId="0" />
</cellStyleXfs> </cellStyleXfs>
<cellXfs count="5"> <cellXfs count="6">
<xf xfId="0" /> <xf xfId="0" />
<xf numFmtId="166" xfId="0" applyNumberFormat="1" /> <xf numFmtId="166" xfId="0" applyNumberFormat="1" />
<xf numFmtId="167" xfId="0" applyNumberFormat="1" /> <xf numFmtId="167" xfId="0" applyNumberFormat="1" />
<xf numFmtId="168" xfId="0" applyNumberFormat="1" /> <xf numFmtId="168" xfId="0" applyNumberFormat="1" />
<xf numFmtId="169" xfId="0" applyNumberFormat="1" /> <xf numFmtId="169" xfId="0" applyNumberFormat="1" />
<xf xfId="0" fontId="1" />
</cellXfs> </cellXfs>
<cellStyles count="1"> <cellStyles count="1">
<cellStyle name="Normal" builtinId="0" xfId="0" /> <cellStyle name="Normal" builtinId="0" xfId="0" />

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<worksheet xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<sheetData>
<row r="1">
<c r="B1" t="inlineStr"><is><t></t></is></c>
<c r="C1" t="inlineStr"><is><t>test string</t></is></c>
<c r="D1"><v>3</v></c>
<c r="E1"><v>3.5</v></c>
<c r="F1" t="b"><v>0</v></c>
<c r="G1" t="b"><v>1</v></c>
</row>
<row r="2">
<c r="A2" t="inlineStr"><is><t>1900-02-28</t></is></c>
<c r="B2" s="1"><v>61</v></c>
<c r="C2" s="3"><v>61.625</v></c>
<c r="D2" s="2"><v>0.625</v></c>
<c r="E2" s="2"><v>0.625</v></c>
<c r="F2" s="4"><v>1</v></c>
</row>
</sheetData>
<autoFilter ref="A1:G1" />
</worksheet>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<worksheet xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<sheetData>
<row r="1">
<c r="B1" t="inlineStr"><is><t></t></is></c>
<c r="C1" t="inlineStr"><is><t>test string</t></is></c>
<c r="D1"><v>3</v></c>
<c r="E1"><v>3.5</v></c>
<c r="F1" t="b"><v>0</v></c>
<c r="G1" t="b"><v>1</v></c>
</row>
<row r="2">
<c r="A2" t="inlineStr"><is><t>1900-02-28</t></is></c>
<c r="B2" s="1"><v>61</v></c>
<c r="C2" s="3"><v>61.625</v></c>
<c r="D2" s="2"><v>0.625</v></c>
<c r="E2" s="2"><v>0.625</v></c>
<c r="F2" s="4"><v>1</v></c>
</row>
</sheetData>
</worksheet>

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<worksheet xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<sheetViews>
<sheetView tabSelected="1" workbookViewId="0">
<pane ySplit="1" topLeftCell="A2" activePane="bottomLeft" state="frozen" />
<selection pane="bottomLeft" />
</sheetView>
</sheetViews>
<sheetData>
<row r="1">
<c r="B1" t="inlineStr"><is><t></t></is></c>
<c r="C1" t="inlineStr"><is><t>test string</t></is></c>
<c r="D1"><v>3</v></c>
<c r="E1"><v>3.5</v></c>
<c r="F1" t="b"><v>0</v></c>
<c r="G1" t="b"><v>1</v></c>
</row>
<row r="2">
<c r="A2" t="inlineStr"><is><t>1900-02-28</t></is></c>
<c r="B2" s="1"><v>61</v></c>
<c r="C2" s="3"><v>61.625</v></c>
<c r="D2" s="2"><v>0.625</v></c>
<c r="E2" s="2"><v>0.625</v></c>
<c r="F2" s="4"><v>1</v></c>
</row>
</sheetData>
</worksheet>

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<worksheet xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<cols>
<col min="1" max="1" width="2.28515625" bestFit="1" customWidth="1" />
<col min="2" max="2" width="2.42578125" bestFit="1" customWidth="1" />
<col min="3" max="3" width="2.5703125" bestFit="1" customWidth="1" />
<col min="4" max="4" width="2.7109375" bestFit="1" customWidth="1" />
<col min="5" max="5" width="2.85546875" bestFit="1" customWidth="1" />
<col min="6" max="6" width="3" bestFit="1" customWidth="1" />
<col min="7" max="7" width="3.140625" bestFit="1" customWidth="1" />
</cols>
<sheetData>
<row r="1">
<c r="B1" t="inlineStr"><is><t></t></is></c>
<c r="C1" t="inlineStr"><is><t>test string</t></is></c>
<c r="D1"><v>3</v></c>
<c r="E1"><v>3.5</v></c>
<c r="F1" t="b"><v>0</v></c>
<c r="G1" t="b"><v>1</v></c>
</row>
<row r="2">
<c r="A2" t="inlineStr"><is><t>1900-02-28</t></is></c>
<c r="B2" s="1"><v>61</v></c>
<c r="C2" s="3"><v>61.625</v></c>
<c r="D2" s="2"><v>0.625</v></c>
<c r="E2" s="2"><v>0.625</v></c>
<c r="F2" s="4"><v>1</v></c>
</row>
</sheetData>
</worksheet>

View File

@@ -16,14 +16,14 @@ using NUnit.Framework;
namespace Microsoft.SqlTools.ServiceLayer.UnitTests.QueryExecution.DataStorage namespace Microsoft.SqlTools.ServiceLayer.UnitTests.QueryExecution.DataStorage
{ {
public partial class SaveAsExcelFileStreamWriterHelperTests : IDisposable public partial class SaveAsExcelFileStreamWriterHelperTests
{ {
private Stream _stream; [Test]
public SaveAsExcelFileStreamWriterHelperTests() public void DefaultSheet()
{ {
_stream = new MemoryStream(); var stream = new MemoryStream();
using (var helper = new SaveAsExcelFileStreamWriterHelper(_stream, true)) using (var helper = new SaveAsExcelFileStreamWriterHelper(stream, true))
using (var sheet = helper.AddSheet()) using (var sheet = helper.AddSheet(null, 7))
{ {
var value = new DbCellValue(); var value = new DbCellValue();
sheet.AddRow(); sheet.AddRow();
@@ -70,64 +70,431 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.QueryExecution.DataStorage
value.RawObject = new TimeSpan(24, 00, 00); value.RawObject = new TimeSpan(24, 00, 00);
sheet.AddCell(value); sheet.AddCell(value);
} }
ContentMatch(stream, "[Content_Types].xml");
ContentMatch(stream, "_rels/.rels");
ContentMatch(stream, "xl/_rels/workbook.xml.rels");
ContentMatch(stream, "xl/styles.xml");
ContentMatch(stream, "xl/workbook.xml");
ContentMatch(stream, "xl/worksheets/sheet1.xml");
stream.Dispose();
}
[Test]
public void SheetWithFrozenHeaderRow()
{
var stream = new MemoryStream();
using (var helper = new SaveAsExcelFileStreamWriterHelper(stream, true))
using (var sheet = helper.AddSheet(null, 7))
{
sheet.FreezeHeaderRow();
var value = new DbCellValue();
sheet.AddRow();
value.IsNull = true;
sheet.AddCell(value);
value.IsNull = false;
value.RawObject = "";
sheet.AddCell(value);
value.RawObject = "test string";
sheet.AddCell(value);
value.RawObject = 3;
sheet.AddCell(value);
value.RawObject = 3.5;
sheet.AddCell(value);
value.RawObject = false;
sheet.AddCell(value);
value.RawObject = true;
sheet.AddCell(value);
sheet.AddRow();
value.RawObject = new DateTime(1900, 2, 28);
sheet.AddCell(value);
value.RawObject = new DateTime(1900, 3, 1);
sheet.AddCell(value);
value.RawObject = new DateTime(1900, 3, 1, 15, 00, 00);
sheet.AddCell(value);
value.RawObject = new DateTime(1, 1, 1, 15, 00, 00);
sheet.AddCell(value);
value.RawObject = new TimeSpan(15, 00, 00);
sheet.AddCell(value);
value.RawObject = new TimeSpan(24, 00, 00);
sheet.AddCell(value);
}
ContentMatch(stream, "[Content_Types].xml");
ContentMatch(stream, "_rels/.rels");
ContentMatch(stream, "xl/_rels/workbook.xml.rels");
ContentMatch(stream, "xl/styles.xml");
ContentMatch(stream, "xl/workbook.xml");
ContentMatch(stream, "xl/worksheets/sheet1.xml", "xl/worksheets/sheet1-headerRowFrozen.xml");
stream.Dispose();
}
[Test]
public void SheetWithFrozenHeaderRowCalledTooLate()
{
var expectedException = new InvalidOperationException("Must be called before calling AddRow");
var actualException = new Exception();
var stream = new MemoryStream();
using (var helper = new SaveAsExcelFileStreamWriterHelper(stream, true))
using (var sheet = helper.AddSheet(null, 7))
{
sheet.AddRow();
try
{
sheet.FreezeHeaderRow();
Assert.Fail("Did not throw an exception when calling FreezeHeaderRow too late");
}
catch (Exception e)
{
actualException = e;
}
}
Assert.AreEqual(expectedException.GetType(), actualException.GetType());
Assert.AreEqual(expectedException.Message, actualException.Message);
stream.Dispose();
}
[Test]
public void SheetWithBoldHeaderRow()
{
var stream = new MemoryStream();
using (var helper = new SaveAsExcelFileStreamWriterHelper(stream, true))
using (var sheet = helper.AddSheet(null, 7))
{
var value = new DbCellValue();
sheet.AddRow();
value.IsNull = true;
sheet.AddCell(value);
value.IsNull = false;
value.RawObject = "";
sheet.AddCell(value);
value.RawObject = "test string";
sheet.AddCell(value);
value.RawObject = 3;
sheet.AddCell(value);
value.RawObject = 3.5;
sheet.AddCell(value);
value.RawObject = false;
sheet.AddCell(value);
value.RawObject = true;
sheet.AddCell(value);
sheet.AddRow();
value.RawObject = new DateTime(1900, 2, 28);
sheet.AddCell(value);
value.RawObject = new DateTime(1900, 3, 1);
sheet.AddCell(value);
value.RawObject = new DateTime(1900, 3, 1, 15, 00, 00);
sheet.AddCell(value);
value.RawObject = new DateTime(1, 1, 1, 15, 00, 00);
sheet.AddCell(value);
value.RawObject = new TimeSpan(15, 00, 00);
sheet.AddCell(value);
value.RawObject = new TimeSpan(24, 00, 00);
sheet.AddCell(value);
}
ContentMatch(stream, "[Content_Types].xml");
ContentMatch(stream, "_rels/.rels");
ContentMatch(stream, "xl/_rels/workbook.xml.rels");
ContentMatch(stream, "xl/styles.xml");
ContentMatch(stream, "xl/workbook.xml");
ContentMatch(stream, "xl/worksheets/sheet1.xml", "xl/worksheets/sheet1-headerRowBold.xml");
stream.Dispose();
}
[Test]
public void SheetWithAutoFilterEnabled()
{
var stream = new MemoryStream();
using (var helper = new SaveAsExcelFileStreamWriterHelper(stream, true))
using (var sheet = helper.AddSheet(null, 7))
{
sheet.EnableAutoFilter();
var value = new DbCellValue();
sheet.AddRow();
value.IsNull = true;
sheet.AddCell(value);
value.IsNull = false;
value.RawObject = "";
sheet.AddCell(value);
value.RawObject = "test string";
sheet.AddCell(value);
value.RawObject = 3;
sheet.AddCell(value);
value.RawObject = 3.5;
sheet.AddCell(value);
value.RawObject = false;
sheet.AddCell(value);
value.RawObject = true;
sheet.AddCell(value);
sheet.AddRow();
value.RawObject = new DateTime(1900, 2, 28);
sheet.AddCell(value);
value.RawObject = new DateTime(1900, 3, 1);
sheet.AddCell(value);
value.RawObject = new DateTime(1900, 3, 1, 15, 00, 00);
sheet.AddCell(value);
value.RawObject = new DateTime(1, 1, 1, 15, 00, 00);
sheet.AddCell(value);
value.RawObject = new TimeSpan(15, 00, 00);
sheet.AddCell(value);
value.RawObject = new TimeSpan(24, 00, 00);
sheet.AddCell(value);
}
ContentMatch(stream, "[Content_Types].xml");
ContentMatch(stream, "_rels/.rels");
ContentMatch(stream, "xl/_rels/workbook.xml.rels");
ContentMatch(stream, "xl/styles.xml");
ContentMatch(stream, "xl/workbook.xml");
ContentMatch(stream, "xl/worksheets/sheet1.xml", "xl/worksheets/sheet1-autoFilterEnabled.xml");
stream.Dispose();
}
[Test]
public void SheetWriteColumnInformationCalledTooLate()
{
var expectedException = new InvalidOperationException("Must be called before calling AddRow");
var actualException = new Exception();
var stream = new MemoryStream();
using (var helper = new SaveAsExcelFileStreamWriterHelper(stream, true))
using (var sheet = helper.AddSheet(null, 7))
{
sheet.AddRow();
try
{
sheet.WriteColumnInformation(new []{ 1F, 2F, 3F, 4F, 5F, 6F, 7F });
Assert.Fail("Did not throw an exception when calling WriteColumnInformation too late");
}
catch (Exception e)
{
actualException = e;
}
}
Assert.AreEqual(expectedException.GetType(), actualException.GetType());
Assert.AreEqual(expectedException.Message, actualException.Message);
stream.Dispose();
}
[Test]
public void SheetWriteColumnInformationWithWrongAmount()
{
var expectedException = new InvalidOperationException("Column count mismatch");
var actualException = new Exception();
var stream = new MemoryStream();
using (var helper = new SaveAsExcelFileStreamWriterHelper(stream, true))
using (var sheet = helper.AddSheet(null, 7))
{
try
{
sheet.WriteColumnInformation(new[] { 1F, 2F, 3F, 4F, 5F });
Assert.Fail("Did not throw an exception when calling WriteColumnInformation with the wrong number of columns");
}
catch (Exception e)
{
actualException = e;
}
var value = new DbCellValue();
sheet.AddRow();
value.IsNull = true;
sheet.AddCell(value);
value.IsNull = false;
value.RawObject = "";
sheet.AddCell(value);
value.RawObject = "test string";
sheet.AddCell(value);
value.RawObject = 3;
sheet.AddCell(value);
value.RawObject = 3.5;
sheet.AddCell(value);
value.RawObject = false;
sheet.AddCell(value);
value.RawObject = true;
sheet.AddCell(value);
sheet.AddRow();
value.RawObject = new DateTime(1900, 2, 28);
sheet.AddCell(value);
value.RawObject = new DateTime(1900, 3, 1);
sheet.AddCell(value);
value.RawObject = new DateTime(1900, 3, 1, 15, 00, 00);
sheet.AddCell(value);
value.RawObject = new DateTime(1, 1, 1, 15, 00, 00);
sheet.AddCell(value);
value.RawObject = new TimeSpan(15, 00, 00);
sheet.AddCell(value);
value.RawObject = new TimeSpan(24, 00, 00);
sheet.AddCell(value);
}
Assert.AreEqual(expectedException.GetType(), actualException.GetType());
Assert.AreEqual(expectedException.Message, actualException.Message);
stream.Dispose();
}
[Test]
public void SheetWriteColumnInformation()
{
var stream = new MemoryStream();
using (var helper = new SaveAsExcelFileStreamWriterHelper(stream, true))
using (var sheet = helper.AddSheet(null, 7))
{
sheet.WriteColumnInformation(new[] { 1F, 2F, 3F, 4F, 5F, 6F, 7F });
var value = new DbCellValue();
sheet.AddRow();
value.IsNull = true;
sheet.AddCell(value);
value.IsNull = false;
value.RawObject = "";
sheet.AddCell(value);
value.RawObject = "test string";
sheet.AddCell(value);
value.RawObject = 3;
sheet.AddCell(value);
value.RawObject = 3.5;
sheet.AddCell(value);
value.RawObject = false;
sheet.AddCell(value);
value.RawObject = true;
sheet.AddCell(value);
sheet.AddRow();
value.RawObject = new DateTime(1900, 2, 28);
sheet.AddCell(value);
value.RawObject = new DateTime(1900, 3, 1);
sheet.AddCell(value);
value.RawObject = new DateTime(1900, 3, 1, 15, 00, 00);
sheet.AddCell(value);
value.RawObject = new DateTime(1, 1, 1, 15, 00, 00);
sheet.AddCell(value);
value.RawObject = new TimeSpan(15, 00, 00);
sheet.AddCell(value);
value.RawObject = new TimeSpan(24, 00, 00);
sheet.AddCell(value);
}
ContentMatch(stream, "[Content_Types].xml");
ContentMatch(stream, "_rels/.rels");
ContentMatch(stream, "xl/_rels/workbook.xml.rels");
ContentMatch(stream, "xl/styles.xml");
ContentMatch(stream, "xl/workbook.xml");
ContentMatch(stream, "xl/worksheets/sheet1.xml", "xl/worksheets/sheet1-withColumns.xml");
stream.Dispose();
} }
[GeneratedRegex("\\r?\\n\\s*")] [GeneratedRegex("\\r?\\n\\s*")]
private static partial Regex GetContentRemoveLinebreakLeadingSpaceRegex(); private static partial Regex GetContentRemoveLinebreakLeadingSpaceRegex();
private void ContentMatch(string fileName) private void ContentMatch(Stream stream, string fileName)
{
ContentMatch(stream, fileName, fileName);
}
private void ContentMatch(Stream stream, string realFileName, string referenceFileName)
{ {
string referencePath = Path.Combine(RunEnvironmentInfo.GetTestDataLocation(), string referencePath = Path.Combine(RunEnvironmentInfo.GetTestDataLocation(),
"DataStorage", "DataStorage",
"SaveAsExcelFileStreamWriterHelperTests", "SaveAsExcelFileStreamWriterHelperTests",
fileName); referenceFileName);
string referenceContent = File.ReadAllText(referencePath); string referenceContent = File.ReadAllText(referencePath);
referenceContent = GetContentRemoveLinebreakLeadingSpaceRegex().Replace(referenceContent, ""); referenceContent = GetContentRemoveLinebreakLeadingSpaceRegex().Replace(referenceContent, "");
using (var zip = new ZipArchive(_stream, ZipArchiveMode.Read, true)) using (var zip = new ZipArchive(stream, ZipArchiveMode.Read, true))
{ {
using (var reader = new StreamReader(zip?.GetEntry(fileName)?.Open()!)) using (var reader = new StreamReader(zip?.GetEntry(realFileName)?.Open()!))
{ {
string realContent = reader.ReadToEnd(); string realContent = reader.ReadToEnd();
Assert.AreEqual(referenceContent, realContent); Assert.AreEqual(referenceContent, realContent);
} }
} }
} }
[Test]
public void CheckContentType()
{
ContentMatch("[Content_Types].xml");
}
[Test]
public void CheckTopRels()
{
ContentMatch("_rels/.rels");
}
[Test]
public void CheckWorkbookRels()
{
ContentMatch("xl/_rels/workbook.xml.rels");
}
[Test]
public void CheckStyles()
{
ContentMatch("xl/styles.xml");
}
[Test]
public void CheckWorkbook()
{
ContentMatch("xl/workbook.xml");
}
[Test]
public void CheckSheet1()
{
ContentMatch("xl/worksheets/sheet1.xml");
}
public void Dispose()
{
_stream.Dispose();
}
} }
public class SaveAsExcelFileStreamWriterHelperReferenceManagerTests public class SaveAsExcelFileStreamWriterHelperReferenceManagerTests