From e1de76e94977e2ef21c19f2184223f01e56b9eb5 Mon Sep 17 00:00:00 2001 From: Mark Carrington <31017244+MarkMpn@users.noreply.github.com> Date: Mon, 24 Jun 2024 08:10:23 +0100 Subject: [PATCH 01/25] Added keyboard shortcuts to switch and close tabs Fixes #457 --- MarkMpn.Sql4Cds.XTB/PluginControl.cs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/MarkMpn.Sql4Cds.XTB/PluginControl.cs b/MarkMpn.Sql4Cds.XTB/PluginControl.cs index 0ddf3dfe..0ac46840 100644 --- a/MarkMpn.Sql4Cds.XTB/PluginControl.cs +++ b/MarkMpn.Sql4Cds.XTB/PluginControl.cs @@ -535,6 +535,27 @@ protected override bool ProcessCmdKey(ref System.Windows.Forms.Message msg, Keys tsbExecute.PerformClick(); else if (keyData == Keys.F4) dockPanel.ActiveAutoHideContent = _properties; + else if (keyData == (Keys.Control | Keys.W)) + { + if (ConfirmBulkClose(new[] { dockPanel.ActiveDocument }, false)) + dockPanel.ActiveDocumentPane.CloseActiveContent(); + } + else if (keyData == (Keys.Control | Keys.PageUp)) + { + var index = dockPanel.ActiveDocumentPane.DisplayingContents.IndexOf(dockPanel.ActiveDocument); + index++; + if (index >= dockPanel.ActiveDocumentPane.DisplayingContents.Count) + index = 0; + ((DockContent)dockPanel.ActiveDocumentPane.DisplayingContents[index]).Activate(); + } + else if (keyData == (Keys.Control | Keys.PageDown)) + { + var index = dockPanel.ActiveDocumentPane.DisplayingContents.IndexOf(dockPanel.ActiveDocument); + index--; + if (index == -1) + index = dockPanel.ActiveDocumentPane.DisplayingContents.Count - 1; + ((DockContent)dockPanel.ActiveDocumentPane.DisplayingContents[index]).Activate(); + } else return base.ProcessCmdKey(ref msg, keyData); From 55ea349a890e204262002dbe85b85dd67faa3fd4 Mon Sep 17 00:00:00 2001 From: Mark Carrington <31017244+MarkMpn@users.noreply.github.com> Date: Mon, 24 Jun 2024 08:56:38 +0100 Subject: [PATCH 02/25] Allow changing editor font and size Fixes #444 --- MarkMpn.Sql4Cds.XTB/DocumentWindowBase.cs | 4 + .../FetchXml2SqlSettingsForm.cs | 12 +-- MarkMpn.Sql4Cds.XTB/FetchXmlControl.cs | 21 ++++- MarkMpn.Sql4Cds.XTB/IDocumentWindow.cs | 2 + MarkMpn.Sql4Cds.XTB/MQueryControl.cs | 16 +++- MarkMpn.Sql4Cds.XTB/PluginControl.cs | 6 ++ MarkMpn.Sql4Cds.XTB/Settings.cs | 4 + MarkMpn.Sql4Cds.XTB/SettingsForm.Designer.cs | 91 +++++++++++++++++-- MarkMpn.Sql4Cds.XTB/SettingsForm.cs | 38 +++++++- MarkMpn.Sql4Cds.XTB/SettingsForm.resx | 2 +- MarkMpn.Sql4Cds.XTB/SqlQueryControl.cs | 19 +++- 11 files changed, 188 insertions(+), 27 deletions(-) diff --git a/MarkMpn.Sql4Cds.XTB/DocumentWindowBase.cs b/MarkMpn.Sql4Cds.XTB/DocumentWindowBase.cs index 1f5d77dd..083fa770 100644 --- a/MarkMpn.Sql4Cds.XTB/DocumentWindowBase.cs +++ b/MarkMpn.Sql4Cds.XTB/DocumentWindowBase.cs @@ -183,5 +183,9 @@ protected override void OnClosing(CancelEventArgs e) } } } + + public virtual void SettingsChanged() + { + } } } diff --git a/MarkMpn.Sql4Cds.XTB/FetchXml2SqlSettingsForm.cs b/MarkMpn.Sql4Cds.XTB/FetchXml2SqlSettingsForm.cs index b0852d65..f762169a 100644 --- a/MarkMpn.Sql4Cds.XTB/FetchXml2SqlSettingsForm.cs +++ b/MarkMpn.Sql4Cds.XTB/FetchXml2SqlSettingsForm.cs @@ -44,8 +44,8 @@ private void SetFetchXmlStyle(Scintilla scintilla) { // Reset the styles scintilla.StyleResetDefault(); - scintilla.Styles[Style.Default].Font = "Consolas"; - scintilla.Styles[Style.Default].Size = 10; + scintilla.Styles[Style.Default].Font = Settings.Instance.EditorFontName; + scintilla.Styles[Style.Default].Size = Settings.Instance.EditorFontSize; scintilla.StyleClearAll(); // Set the XML Lexer @@ -89,8 +89,8 @@ private void SetFetchXmlStyle(Scintilla scintilla) // Set the Styles scintilla.StyleResetDefault(); // I like fixed font for XML - scintilla.Styles[Style.Default].Font = "Courier New"; - scintilla.Styles[Style.Default].Size = 10; + scintilla.Styles[Style.Default].Font = Settings.Instance.EditorFontName; + scintilla.Styles[Style.Default].Size = Settings.Instance.EditorFontSize; scintilla.StyleClearAll(); scintilla.Styles[Style.Xml.Attribute].ForeColor = Color.Red; scintilla.Styles[Style.Xml.Entity].ForeColor = Color.Red; @@ -106,8 +106,8 @@ private void SetFetchXmlStyle(Scintilla scintilla) private void SetSqlStyle(Scintilla scintilla) { scintilla.StyleResetDefault(); - scintilla.Styles[Style.Default].Font = "Courier New"; - scintilla.Styles[Style.Default].Size = 10; + scintilla.Styles[Style.Default].Font = Settings.Instance.EditorFontName; + scintilla.Styles[Style.Default].Size = Settings.Instance.EditorFontSize; scintilla.StyleClearAll(); scintilla.Lexer = Lexer.Sql; diff --git a/MarkMpn.Sql4Cds.XTB/FetchXmlControl.cs b/MarkMpn.Sql4Cds.XTB/FetchXmlControl.cs index 963c9a8f..1f208f05 100644 --- a/MarkMpn.Sql4Cds.XTB/FetchXmlControl.cs +++ b/MarkMpn.Sql4Cds.XTB/FetchXmlControl.cs @@ -42,8 +42,8 @@ public FetchXmlControl() // Reset the styles scintilla.StyleResetDefault(); - scintilla.Styles[Style.Default].Font = "Consolas"; - scintilla.Styles[Style.Default].Size = 10; + scintilla.Styles[Style.Default].Font = Settings.Instance.EditorFontName; + scintilla.Styles[Style.Default].Size = Settings.Instance.EditorFontSize; scintilla.StyleClearAll(); // Set the XML Lexer @@ -86,9 +86,8 @@ public FetchXmlControl() // Set the Styles scintilla.StyleResetDefault(); - // I like fixed font for XML - scintilla.Styles[Style.Default].Font = "Courier New"; - scintilla.Styles[Style.Default].Size = 10; + scintilla.Styles[Style.Default].Font = Settings.Instance.EditorFontName; + scintilla.Styles[Style.Default].Size = Settings.Instance.EditorFontSize; scintilla.StyleClearAll(); scintilla.Styles[Style.Xml.Attribute].ForeColor = Color.Red; scintilla.Styles[Style.Xml.Entity].ForeColor = Color.Red; @@ -104,6 +103,18 @@ public void SetFocus() scintilla.Focus(); } + public override void SettingsChanged() + { + base.SettingsChanged(); + + // Update all styles on the editor to use the new font + foreach (var style in scintilla.Styles) + { + style.Font = Settings.Instance.EditorFontName; + style.Size = Settings.Instance.EditorFontSize; + } + } + protected override string Type => "FetchXML"; public override string Content diff --git a/MarkMpn.Sql4Cds.XTB/IDocumentWindow.cs b/MarkMpn.Sql4Cds.XTB/IDocumentWindow.cs index 1e667dfb..73814576 100644 --- a/MarkMpn.Sql4Cds.XTB/IDocumentWindow.cs +++ b/MarkMpn.Sql4Cds.XTB/IDocumentWindow.cs @@ -13,6 +13,8 @@ interface IDocumentWindow TabContent GetSessionDetails(); void RestoreSessionDetails(TabContent tab); + + void SettingsChanged(); } interface ISaveableDocumentWindow : IDocumentWindow diff --git a/MarkMpn.Sql4Cds.XTB/MQueryControl.cs b/MarkMpn.Sql4Cds.XTB/MQueryControl.cs index fd0e68d4..630d8387 100644 --- a/MarkMpn.Sql4Cds.XTB/MQueryControl.cs +++ b/MarkMpn.Sql4Cds.XTB/MQueryControl.cs @@ -31,8 +31,8 @@ public MQueryControl() // Configuring the default style with properties // we have common to every lexer style saves time. scintilla.StyleResetDefault(); - scintilla.Styles[Style.Default].Font = "Consolas"; - scintilla.Styles[Style.Default].Size = 10; + scintilla.Styles[Style.Default].Font = Settings.Instance.EditorFontName; + scintilla.Styles[Style.Default].Size = Settings.Instance.EditorFontSize; scintilla.StyleClearAll(); // Configure the CPP (C#) lexer styles @@ -73,5 +73,17 @@ public void SetFocus() { scintilla.Focus(); } + + public override void SettingsChanged() + { + base.SettingsChanged(); + + // Update all styles on the editor to use the new font + foreach (var style in scintilla.Styles) + { + style.Font = Settings.Instance.EditorFontName; + style.Size = Settings.Instance.EditorFontSize; + } + } } } diff --git a/MarkMpn.Sql4Cds.XTB/PluginControl.cs b/MarkMpn.Sql4Cds.XTB/PluginControl.cs index 0ac46840..59a24c3f 100644 --- a/MarkMpn.Sql4Cds.XTB/PluginControl.cs +++ b/MarkMpn.Sql4Cds.XTB/PluginControl.cs @@ -632,7 +632,13 @@ public void ShowSettings() using (var form = new SettingsForm(Settings.Instance, this)) { if (form.ShowDialog(this) == DialogResult.OK) + { SaveSettings(); + + // Notify each document of the new settings + foreach (var doc in dockPanel.Contents.OfType()) + doc.SettingsChanged(); + } } } diff --git a/MarkMpn.Sql4Cds.XTB/Settings.cs b/MarkMpn.Sql4Cds.XTB/Settings.cs index 206b37f6..bac0be63 100644 --- a/MarkMpn.Sql4Cds.XTB/Settings.cs +++ b/MarkMpn.Sql4Cds.XTB/Settings.cs @@ -66,6 +66,10 @@ public class Settings public ColumnOrdering ColumnOrdering { get; set; } = ColumnOrdering.Alphabetical; public string DockLayout { get; set; } + + public string EditorFontName { get; set; } = "Courier New"; + + public int EditorFontSize { get; set; } = 10; } public class TabContent diff --git a/MarkMpn.Sql4Cds.XTB/SettingsForm.Designer.cs b/MarkMpn.Sql4Cds.XTB/SettingsForm.Designer.cs index 9a7b8942..c5eb45fb 100644 --- a/MarkMpn.Sql4Cds.XTB/SettingsForm.Designer.cs +++ b/MarkMpn.Sql4Cds.XTB/SettingsForm.Designer.cs @@ -78,6 +78,7 @@ private void InitializeComponent() this.label16 = new System.Windows.Forms.Label(); this.pictureBox3 = new System.Windows.Forms.PictureBox(); this.tabPage3 = new System.Windows.Forms.TabPage(); + this.resetToolWindowsButton = new System.Windows.Forms.Button(); this.localDateFormatCheckbox = new System.Windows.Forms.CheckBox(); this.rememberSessionCheckbox = new System.Windows.Forms.CheckBox(); this.tabPage4 = new System.Windows.Forms.TabPage(); @@ -87,7 +88,11 @@ private void InitializeComponent() this.nativeSqlRadioButton = new System.Windows.Forms.RadioButton(); this.simpleSqlRadioButton = new System.Windows.Forms.RadioButton(); this.label17 = new System.Windows.Forms.Label(); - this.resetToolWindowsButton = new System.Windows.Forms.Button(); + this.label18 = new System.Windows.Forms.Label(); + this.fontComboBox = new System.Windows.Forms.ComboBox(); + this.label19 = new System.Windows.Forms.Label(); + this.fontSizeNumericUpDown = new System.Windows.Forms.NumericUpDown(); + this.label20 = new System.Windows.Forms.Label(); this.topPanel.SuspendLayout(); ((System.ComponentModel.ISupportInitialize)(this.pictureBox)).BeginInit(); this.panel2.SuspendLayout(); @@ -109,6 +114,7 @@ private void InitializeComponent() ((System.ComponentModel.ISupportInitialize)(this.pictureBox3)).BeginInit(); this.tabPage3.SuspendLayout(); this.tabPage4.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.fontSizeNumericUpDown)).BeginInit(); this.SuspendLayout(); // // topPanel @@ -675,6 +681,11 @@ private void InitializeComponent() // // tabPage3 // + this.tabPage3.Controls.Add(this.label20); + this.tabPage3.Controls.Add(this.fontSizeNumericUpDown); + this.tabPage3.Controls.Add(this.label19); + this.tabPage3.Controls.Add(this.fontComboBox); + this.tabPage3.Controls.Add(this.label18); this.tabPage3.Controls.Add(this.resetToolWindowsButton); this.tabPage3.Controls.Add(this.localDateFormatCheckbox); this.tabPage3.Controls.Add(this.rememberSessionCheckbox); @@ -688,6 +699,16 @@ private void InitializeComponent() this.tabPage3.Text = "Editor"; this.tabPage3.UseVisualStyleBackColor = true; // + // resetToolWindowsButton + // + this.resetToolWindowsButton.Location = new System.Drawing.Point(6, 98); + this.resetToolWindowsButton.Name = "resetToolWindowsButton"; + this.resetToolWindowsButton.Size = new System.Drawing.Size(119, 23); + this.resetToolWindowsButton.TabIndex = 4; + this.resetToolWindowsButton.Text = "Reset Tool Windows"; + this.resetToolWindowsButton.UseVisualStyleBackColor = true; + this.resetToolWindowsButton.Click += new System.EventHandler(this.resetToolWindowsButton_Click); + // // localDateFormatCheckbox // this.localDateFormatCheckbox.AutoSize = true; @@ -788,15 +809,61 @@ private void InitializeComponent() this.label17.TabIndex = 0; this.label17.Text = "When converting FetchXML to SQL (e.g. from FetchXML Builder):"; // - // resetToolWindowsButton + // label18 // - this.resetToolWindowsButton.Location = new System.Drawing.Point(6, 98); - this.resetToolWindowsButton.Name = "resetToolWindowsButton"; - this.resetToolWindowsButton.Size = new System.Drawing.Size(119, 23); - this.resetToolWindowsButton.TabIndex = 4; - this.resetToolWindowsButton.Text = "Reset Tool Windows"; - this.resetToolWindowsButton.UseVisualStyleBackColor = true; - this.resetToolWindowsButton.Click += new System.EventHandler(this.resetToolWindowsButton_Click); + this.label18.AutoSize = true; + this.label18.Location = new System.Drawing.Point(6, 133); + this.label18.Name = "label18"; + this.label18.Size = new System.Drawing.Size(58, 13); + this.label18.TabIndex = 5; + this.label18.Text = "Editor Font"; + // + // fontComboBox + // + this.fontComboBox.AutoCompleteMode = System.Windows.Forms.AutoCompleteMode.SuggestAppend; + this.fontComboBox.AutoCompleteSource = System.Windows.Forms.AutoCompleteSource.ListItems; + this.fontComboBox.DrawMode = System.Windows.Forms.DrawMode.OwnerDrawFixed; + this.fontComboBox.FormattingEnabled = true; + this.fontComboBox.Location = new System.Drawing.Point(9, 149); + this.fontComboBox.Name = "fontComboBox"; + this.fontComboBox.Size = new System.Drawing.Size(200, 21); + this.fontComboBox.TabIndex = 6; + this.fontComboBox.DrawItem += new System.Windows.Forms.DrawItemEventHandler(this.fontComboBox_DrawItem); + // + // label19 + // + this.label19.AutoSize = true; + this.label19.Location = new System.Drawing.Point(212, 134); + this.label19.Name = "label19"; + this.label19.Size = new System.Drawing.Size(51, 13); + this.label19.TabIndex = 7; + this.label19.Text = "Font Size"; + // + // fontSizeNumericUpDown + // + this.fontSizeNumericUpDown.Location = new System.Drawing.Point(215, 150); + this.fontSizeNumericUpDown.Minimum = new decimal(new int[] { + 1, + 0, + 0, + 0}); + this.fontSizeNumericUpDown.Name = "fontSizeNumericUpDown"; + this.fontSizeNumericUpDown.Size = new System.Drawing.Size(38, 20); + this.fontSizeNumericUpDown.TabIndex = 8; + this.fontSizeNumericUpDown.Value = new decimal(new int[] { + 10, + 0, + 0, + 0}); + // + // label20 + // + this.label20.AutoSize = true; + this.label20.Location = new System.Drawing.Point(259, 152); + this.label20.Name = "label20"; + this.label20.Size = new System.Drawing.Size(16, 13); + this.label20.TabIndex = 9; + this.label20.Text = "pt"; // // SettingsForm // @@ -843,6 +910,7 @@ private void InitializeComponent() this.tabPage3.PerformLayout(); this.tabPage4.ResumeLayout(false); this.tabPage4.PerformLayout(); + ((System.ComponentModel.ISupportInitialize)(this.fontSizeNumericUpDown)).EndInit(); this.ResumeLayout(false); } @@ -908,5 +976,10 @@ private void InitializeComponent() private System.Windows.Forms.LinkLabel fetchXml2SqlConversionAdvancedLinkLabel; private System.Windows.Forms.CheckBox schemaColumnOrderingCheckbox; private System.Windows.Forms.Button resetToolWindowsButton; + private System.Windows.Forms.NumericUpDown fontSizeNumericUpDown; + private System.Windows.Forms.Label label19; + private System.Windows.Forms.ComboBox fontComboBox; + private System.Windows.Forms.Label label18; + private System.Windows.Forms.Label label20; } } \ No newline at end of file diff --git a/MarkMpn.Sql4Cds.XTB/SettingsForm.cs b/MarkMpn.Sql4Cds.XTB/SettingsForm.cs index 4325d6e3..536185bc 100644 --- a/MarkMpn.Sql4Cds.XTB/SettingsForm.cs +++ b/MarkMpn.Sql4Cds.XTB/SettingsForm.cs @@ -22,6 +22,10 @@ public SettingsForm(Settings settings, PluginControl plugin) { InitializeComponent(); + fontComboBox.DataSource = FontFamily.Families; + fontComboBox.ValueMember = nameof(FontFamily.Name); + fontComboBox.DisplayMember = nameof(FontFamily.Name); + quotedIdentifiersCheckbox.Checked = settings.QuotedIdentifiers; selectLimitUpDown.Value = settings.SelectLimit; retriveLimitUpDown.Value = settings.MaxRetrievesPerQuery; @@ -44,6 +48,8 @@ public SettingsForm(Settings settings, PluginControl plugin) simpleSqlRadioButton.Checked = !settings.UseNativeSqlConversion; nativeSqlRadioButton.Checked = settings.UseNativeSqlConversion; schemaColumnOrderingCheckbox.Checked = settings.ColumnOrdering == ColumnOrdering.Strict; + fontComboBox.SelectedValue = Settings.Instance.EditorFontName; + fontSizeNumericUpDown.Value = Settings.Instance.EditorFontSize; SetSqlStyle(simpleSqlScintilla); SetSqlStyle(nativeSqlScintilla); @@ -61,8 +67,8 @@ public SettingsForm(Settings settings, PluginControl plugin) private void SetSqlStyle(Scintilla scintilla) { scintilla.StyleResetDefault(); - scintilla.Styles[Style.Default].Font = "Courier New"; - scintilla.Styles[Style.Default].Size = 10; + scintilla.Styles[Style.Default].Font = Settings.Instance.EditorFontName; + scintilla.Styles[Style.Default].Size = Settings.Instance.EditorFontSize; scintilla.StyleClearAll(); scintilla.Lexer = Lexer.Sql; @@ -123,6 +129,8 @@ protected override void OnClosing(CancelEventArgs e) _settings.UseNativeSqlConversion = nativeSqlRadioButton.Checked; _settings.FetchXml2SqlOptions = _fetchXml2SqlOptions; _settings.ColumnOrdering = schemaColumnOrderingCheckbox.Checked ? ColumnOrdering.Strict : ColumnOrdering.Alphabetical; + _settings.EditorFontName = (string)fontComboBox.SelectedValue ?? "Courier New"; + _settings.EditorFontSize = (int) fontSizeNumericUpDown.Value; } } @@ -172,5 +180,31 @@ private void resetToolWindowsButton_Click(object sender, EventArgs e) { _pluginControl.ResetDockLayout(); } + + private void fontComboBox_DrawItem(object sender, DrawItemEventArgs e) + { + var ff = (FontFamily)fontComboBox.Items[e.Index]; + + using (var font = new Font(ff, fontComboBox.Font.Size)) + { + e.DrawBackground(); + + var monospaceIndictorSize = 10; + var monospaceIndictorOffset = (e.Bounds.Height - monospaceIndictorSize) / 2; + + if (e.Graphics.MeasureString("i", font).Width == e.Graphics.MeasureString("W", font).Width) + { + // Monospaced font + e.Graphics.FillEllipse(Brushes.Green, e.Bounds.X + monospaceIndictorOffset, e.Bounds.Y + monospaceIndictorOffset, monospaceIndictorSize, monospaceIndictorSize); + } + else + { + // Variable width font + } + + e.Graphics.DrawString(ff.Name, font, Brushes.Black, e.Bounds.Location.X + monospaceIndictorOffset + monospaceIndictorSize + monospaceIndictorOffset, e.Bounds.Location.Y); + e.DrawFocusRectangle(); + } + } } } diff --git a/MarkMpn.Sql4Cds.XTB/SettingsForm.resx b/MarkMpn.Sql4Cds.XTB/SettingsForm.resx index 1bb59ab3..1d366157 100644 --- a/MarkMpn.Sql4Cds.XTB/SettingsForm.resx +++ b/MarkMpn.Sql4Cds.XTB/SettingsForm.resx @@ -121,7 +121,7 @@ iVBORw0KGgoAAAANSUhEUgAAAFAAAABQCAIAAAABc2X6AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO - vgAADr4B6kKxwAAAF2tJREFUeF7tm3l0leW1xvnj1laLDCKEUQwIARWrogwVrSBeiorKnHk6mUMCBAJh + vQAADr0BR/uQrQAAF2tJREFUeF7tm3l0leW1xvnj1laLDCKEUQwIARWrogwVrSBeiorKnHk6mUMCBAJh SIwiQ8KQhEwQAhhAkRlEnEASBUEqoG2Ve22VoXVoa+vqogp6r7X3+c7vZOflBOqw7l+3l/Ws3T08e797 f+/7DQdsix+26vAvhRY/ah1y2ZXtBSmXt+kolxScLq85jIMiKV0VBMuVjt+YAqsQAi5BEshjZQWyBHnk NxMPijlJvLyt10mj7CT5w1YhLX7Q8mpji+Tx2nS8om0nnM0BQYrxBX8tL+qV9s8sSFcIv0WDPJSiAkUo diff --git a/MarkMpn.Sql4Cds.XTB/SqlQueryControl.cs b/MarkMpn.Sql4Cds.XTB/SqlQueryControl.cs index 3ea056dd..c53f4c93 100644 --- a/MarkMpn.Sql4Cds.XTB/SqlQueryControl.cs +++ b/MarkMpn.Sql4Cds.XTB/SqlQueryControl.cs @@ -145,6 +145,21 @@ public override string Content public string Sql => String.IsNullOrEmpty(_editor.SelectedText) ? _editor.Text : _editor.SelectedText; + public override void SettingsChanged() + { + base.SettingsChanged(); + + // Update all styles on the editor to use the new font + foreach (var style in _editor.Styles) + { + style.Font = Settings.Instance.EditorFontName; + style.Size = Settings.Instance.EditorFontSize; + } + + // Update the font on the autocomplete menu as well + _autocomplete.Font = new Font(Settings.Instance.EditorFontName, Settings.Instance.EditorFontSize); + } + protected override void OnClosing(CancelEventArgs e) { if (Busy) @@ -260,8 +275,8 @@ private Scintilla CreateEditor() // Reset the styles scintilla.StyleResetDefault(); - scintilla.Styles[Style.Default].Font = "Courier New"; - scintilla.Styles[Style.Default].Size = 10; + scintilla.Styles[Style.Default].Font = Settings.Instance.EditorFontName; + scintilla.Styles[Style.Default].Size = Settings.Instance.EditorFontSize; scintilla.StyleClearAll(); return scintilla; From 9ff0f4109a6275bc97eff05b0bcee58d9ee235b8 Mon Sep 17 00:00:00 2001 From: Mark Carrington <31017244+MarkMpn@users.noreply.github.com> Date: Tue, 25 Jun 2024 20:51:05 +0100 Subject: [PATCH 03/25] Fixed incorrect type conversion errors when using specialized FetchXML comparison conditions --- .../ExecutionPlan/FetchXmlScan.cs | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/MarkMpn.Sql4Cds.Engine/ExecutionPlan/FetchXmlScan.cs b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/FetchXmlScan.cs index 499894e0..44a7a1ed 100644 --- a/MarkMpn.Sql4Cds.Engine/ExecutionPlan/FetchXmlScan.cs +++ b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/FetchXmlScan.cs @@ -474,6 +474,39 @@ private void VerifyFilterValueTypes(string entityName, object[] items, DataSourc var attrType = attr.GetAttributeSqlType(dataSource, false); if (attrType.IsSameAs(DataTypeHelpers.EntityReference)) attrType = DataTypeHelpers.UniqueIdentifier; + + // For some operators the value type may be different from the attribute type + switch (condition.@operator) + { + case @operator.infiscalperiod: + case @operator.infiscalperiodandyear: + case @operator.infiscalyear: + case @operator.inorafterfiscalperiodandyear: + case @operator.inorbeforefiscalperiodandyear: + case @operator.lastxdays: + case @operator.lastxfiscalperiods: + case @operator.lastxfiscalyears: + case @operator.lastxhours: + case @operator.lastxmonths: + case @operator.lastxweeks: + case @operator.lastxyears: + case @operator.nextxdays: + case @operator.nextxfiscalperiods: + case @operator.nextxfiscalyears: + case @operator.nextxhours: + case @operator.nextxmonths: + case @operator.nextxweeks: + case @operator.nextxyears: + case @operator.olderthanxdays: + case @operator.olderthanxhours: + case @operator.olderthanxminutes: + case @operator.olderthanxmonths: + case @operator.olderthanxweeks: + case @operator.olderthanxyears: + attrType = DataTypeHelpers.Int; + break; + } + var conversion = SqlTypeConverter.GetConversion(DataTypeHelpers.NVarChar(Int32.MaxValue, dataSource.DefaultCollation, CollationLabel.CoercibleDefault), attrType); if (condition.value != null) From 43284723e55bd9bf66ae291e8f53fb37c670f28d Mon Sep 17 00:00:00 2001 From: Mark Carrington <31017244+MarkMpn@users.noreply.github.com> Date: Tue, 25 Jun 2024 21:44:25 +0100 Subject: [PATCH 04/25] Fixed exposing collation of CAST/CONVERT to string --- MarkMpn.Sql4Cds.Engine.Tests/AdoProviderTests.cs | 13 +++++++++++++ .../ExecutionPlan/ExpressionExtensions.cs | 6 +++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/MarkMpn.Sql4Cds.Engine.Tests/AdoProviderTests.cs b/MarkMpn.Sql4Cds.Engine.Tests/AdoProviderTests.cs index 46c713d5..c373d5cc 100644 --- a/MarkMpn.Sql4Cds.Engine.Tests/AdoProviderTests.cs +++ b/MarkMpn.Sql4Cds.Engine.Tests/AdoProviderTests.cs @@ -2075,5 +2075,18 @@ IF EXISTS(SELECT * FROM account WHERE name = 'Data8') Assert.AreEqual("Not Exists", message); } } + + [TestMethod] + public void StringAggCast() + { + using (var con = new Sql4CdsConnection(_localDataSources)) + { + var result = con.ExecuteScalar(@" + SELECT STRING_AGG(CAST(ID AS NVARCHAR(MAX)), ',') + FROM (VALUES (1), (2), (3)) AS T(ID)"); + + Assert.AreEqual("1,2,3", result); + } + } } } diff --git a/MarkMpn.Sql4Cds.Engine/ExecutionPlan/ExpressionExtensions.cs b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/ExpressionExtensions.cs index 1d3b3bf4..8eb30656 100644 --- a/MarkMpn.Sql4Cds.Engine/ExecutionPlan/ExpressionExtensions.cs +++ b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/ExpressionExtensions.cs @@ -2032,10 +2032,10 @@ private static Expression ToExpression(this ConvertCall convert, ExpressionCompi sqlType = convert.DataType; - return Convert(context, value, valueType, valueCacheKey, sqlType, style, styleType, styleCacheKey, convert, "CONVERT", out cacheKey); + return Convert(context, value, valueType, valueCacheKey, ref sqlType, style, styleType, styleCacheKey, convert, "CONVERT", out cacheKey); } - private static Expression Convert(ExpressionCompilationContext context, Expression value, DataTypeReference valueType, string valueCacheKey, DataTypeReference sqlType, Expression style, DataTypeReference styleType, string styleCacheKey, TSqlFragment expr, string cacheKeyRoot, out string cacheKey) + private static Expression Convert(ExpressionCompilationContext context, Expression value, DataTypeReference valueType, string valueCacheKey, ref DataTypeReference sqlType, Expression style, DataTypeReference styleType, string styleCacheKey, TSqlFragment expr, string cacheKeyRoot, out string cacheKey) { if (sqlType is SqlDataTypeReference sqlTargetType && sqlTargetType.SqlDataTypeOption.IsStringType()) @@ -2072,7 +2072,7 @@ private static Expression ToExpression(this CastCall cast, ExpressionCompilation var value = cast.InvokeSubExpression(x => x.Parameter, x => x.Parameter, context, contextParam, exprParam, createExpression, out var valueType, out var valueCacheKey); sqlType = cast.DataType; - return Convert(context, value, valueType, valueCacheKey, sqlType, null, null, null, cast, "CAST", out cacheKey); + return Convert(context, value, valueType, valueCacheKey, ref sqlType, null, null, null, cast, "CAST", out cacheKey); } private static readonly Regex _containsParser = new Regex("^\\S+( OR \\S+)*$", RegexOptions.IgnoreCase | RegexOptions.Compiled); From b6664509d71a630f434d554ded7069015f5365d6 Mon Sep 17 00:00:00 2001 From: Mark Carrington <31017244+MarkMpn@users.noreply.github.com> Date: Tue, 25 Jun 2024 21:44:45 +0100 Subject: [PATCH 05/25] Improved reporting of error when using SELECT * in scalar subquery --- .../AdoProviderTests.cs | 17 +++++++++++++++++ MarkMpn.Sql4Cds.Engine/ExecutionPlanBuilder.cs | 2 ++ 2 files changed, 19 insertions(+) diff --git a/MarkMpn.Sql4Cds.Engine.Tests/AdoProviderTests.cs b/MarkMpn.Sql4Cds.Engine.Tests/AdoProviderTests.cs index c373d5cc..2c707085 100644 --- a/MarkMpn.Sql4Cds.Engine.Tests/AdoProviderTests.cs +++ b/MarkMpn.Sql4Cds.Engine.Tests/AdoProviderTests.cs @@ -2088,5 +2088,22 @@ SELECT STRING_AGG(CAST(ID AS NVARCHAR(MAX)), ',') Assert.AreEqual("1,2,3", result); } } + + [TestMethod] + public void InSelectStarError() + { + using (var con = new Sql4CdsConnection(_localDataSources)) + { + try + { + con.Execute("SELECT * FROM contact WHERE contactid IN (SELECT * FROM account)"); + Assert.Fail(); + } + catch (Sql4CdsException ex) + { + Assert.AreEqual(116, ex.Number); + } + } + } } } diff --git a/MarkMpn.Sql4Cds.Engine/ExecutionPlanBuilder.cs b/MarkMpn.Sql4Cds.Engine/ExecutionPlanBuilder.cs index 6cb58de2..e4f1420f 100644 --- a/MarkMpn.Sql4Cds.Engine/ExecutionPlanBuilder.cs +++ b/MarkMpn.Sql4Cds.Engine/ExecutionPlanBuilder.cs @@ -2927,6 +2927,8 @@ private IDataExecutionPlanNodeInternal ConvertInSubquery(IDataExecutionPlanNodeI var innerQuery = ConvertSelectStatement(inSubquery.Subquery.QueryExpression, hints, schema, references, innerContext); // Scalar subquery must return exactly one column and one row + innerQuery.ExpandWildcardColumns(innerContext); + if (innerQuery.ColumnSet.Count != 1) throw new NotSupportedQueryFragmentException(Sql4CdsError.MultiColumnScalarSubquery(inSubquery.Subquery)); From 8f8714d32b69abc705e720467e796189bc5caf2e Mon Sep 17 00:00:00 2001 From: Mark Carrington <31017244+MarkMpn@users.noreply.github.com> Date: Tue, 25 Jun 2024 21:45:30 +0100 Subject: [PATCH 06/25] Improved error reporting when using SELECT * in scalar subquery --- MarkMpn.Sql4Cds.Engine/ExecutionPlanBuilder.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MarkMpn.Sql4Cds.Engine/ExecutionPlanBuilder.cs b/MarkMpn.Sql4Cds.Engine/ExecutionPlanBuilder.cs index e4f1420f..e539b613 100644 --- a/MarkMpn.Sql4Cds.Engine/ExecutionPlanBuilder.cs +++ b/MarkMpn.Sql4Cds.Engine/ExecutionPlanBuilder.cs @@ -3935,6 +3935,8 @@ private ColumnReferenceExpression ConvertScalarSubqueries(TSqlFragment expressio var subqueryPlan = ConvertSelectStatement(subquery.QueryExpression, hints, outerSchema, outerReferences, innerContext); // Scalar subquery must return exactly one column and one row + subqueryPlan.ExpandWildcardColumns(); + if (subqueryPlan.ColumnSet.Count != 1) throw new NotSupportedQueryFragmentException(Sql4CdsError.MultiColumnScalarSubquery(subquery)); From 6008bb4ea199532ec56342a3bc67b870b0cc2beb Mon Sep 17 00:00:00 2001 From: Mark Carrington <31017244+MarkMpn@users.noreply.github.com> Date: Tue, 25 Jun 2024 21:45:42 +0100 Subject: [PATCH 07/25] Fixed build error --- MarkMpn.Sql4Cds.Engine/ExecutionPlanBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MarkMpn.Sql4Cds.Engine/ExecutionPlanBuilder.cs b/MarkMpn.Sql4Cds.Engine/ExecutionPlanBuilder.cs index e539b613..260c9e71 100644 --- a/MarkMpn.Sql4Cds.Engine/ExecutionPlanBuilder.cs +++ b/MarkMpn.Sql4Cds.Engine/ExecutionPlanBuilder.cs @@ -3935,7 +3935,7 @@ private ColumnReferenceExpression ConvertScalarSubqueries(TSqlFragment expressio var subqueryPlan = ConvertSelectStatement(subquery.QueryExpression, hints, outerSchema, outerReferences, innerContext); // Scalar subquery must return exactly one column and one row - subqueryPlan.ExpandWildcardColumns(); + subqueryPlan.ExpandWildcardColumns(innerContext); if (subqueryPlan.ColumnSet.Count != 1) throw new NotSupportedQueryFragmentException(Sql4CdsError.MultiColumnScalarSubquery(subquery)); From e0dd417711107fd1605ae2039174e8477dfd66d7 Mon Sep 17 00:00:00 2001 From: Mark Carrington <31017244+MarkMpn@users.noreply.github.com> Date: Tue, 25 Jun 2024 21:54:42 +0100 Subject: [PATCH 08/25] Improved error reporting on aggregate parameter counts --- .../AdoProviderTests.cs | 17 +++++++++++++++++ MarkMpn.Sql4Cds.Engine/ExecutionPlanBuilder.cs | 18 +++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/MarkMpn.Sql4Cds.Engine.Tests/AdoProviderTests.cs b/MarkMpn.Sql4Cds.Engine.Tests/AdoProviderTests.cs index 2c707085..2df8bcda 100644 --- a/MarkMpn.Sql4Cds.Engine.Tests/AdoProviderTests.cs +++ b/MarkMpn.Sql4Cds.Engine.Tests/AdoProviderTests.cs @@ -2105,5 +2105,22 @@ public void InSelectStarError() } } } + + [TestMethod] + public void MissingCountParameter() + { + using (var con = new Sql4CdsConnection(_localDataSources)) + { + try + { + con.Execute("SELECT count() FROM account"); + Assert.Fail(); + } + catch (Sql4CdsException ex) + { + Assert.AreEqual(174, ex.Number); + } + } + } } } diff --git a/MarkMpn.Sql4Cds.Engine/ExecutionPlanBuilder.cs b/MarkMpn.Sql4Cds.Engine/ExecutionPlanBuilder.cs index 260c9e71..c94b428c 100644 --- a/MarkMpn.Sql4Cds.Engine/ExecutionPlanBuilder.cs +++ b/MarkMpn.Sql4Cds.Engine/ExecutionPlanBuilder.cs @@ -3347,15 +3347,22 @@ private IDataExecutionPlanNodeInternal ConvertGroupByAggregates(IDataExecutionPl Distinct = aggregate.Expression.UniqueRowFilter == UniqueRowFilter.Distinct }; - converted.SqlExpression = aggregate.Expression.Parameters[0].Clone(); + if (aggregate.Expression.Parameters.Count > 0) + converted.SqlExpression = aggregate.Expression.Parameters[0].Clone(); switch (aggregate.Expression.FunctionName.Value.ToUpper()) { case "AVG": + if (aggregate.Expression.Parameters.Count != 1) + throw new NotSupportedQueryFragmentException(Sql4CdsError.InvalidFunctionParameterCount(aggregate.Expression.FunctionName, 1)); + converted.AggregateType = AggregateType.Average; break; case "COUNT": + if (aggregate.Expression.Parameters.Count != 1) + throw new NotSupportedQueryFragmentException(Sql4CdsError.InvalidFunctionParameterCount(aggregate.Expression.FunctionName, 1)); + if ((converted.SqlExpression is ColumnReferenceExpression countCol && countCol.ColumnType == ColumnType.Wildcard) || (converted.SqlExpression is Literal && !(converted.SqlExpression is NullLiteral))) converted.AggregateType = AggregateType.CountStar; else @@ -3363,14 +3370,23 @@ private IDataExecutionPlanNodeInternal ConvertGroupByAggregates(IDataExecutionPl break; case "MAX": + if (aggregate.Expression.Parameters.Count != 1) + throw new NotSupportedQueryFragmentException(Sql4CdsError.InvalidFunctionParameterCount(aggregate.Expression.FunctionName, 1)); + converted.AggregateType = AggregateType.Max; break; case "MIN": + if (aggregate.Expression.Parameters.Count != 1) + throw new NotSupportedQueryFragmentException(Sql4CdsError.InvalidFunctionParameterCount(aggregate.Expression.FunctionName, 1)); + converted.AggregateType = AggregateType.Min; break; case "SUM": + if (aggregate.Expression.Parameters.Count != 1) + throw new NotSupportedQueryFragmentException(Sql4CdsError.InvalidFunctionParameterCount(aggregate.Expression.FunctionName, 1)); + if (converted.SqlExpression is IntegerLiteral sumLiteral && sumLiteral.Value == "1") converted.AggregateType = AggregateType.CountStar; else From 680bb82c3f7313acd39285fd2b25bff78ad23ff0 Mon Sep 17 00:00:00 2001 From: Mark Carrington <31017244+MarkMpn@users.noreply.github.com> Date: Tue, 25 Jun 2024 22:21:03 +0100 Subject: [PATCH 09/25] Improved error reporting for invalid operator types --- .../AdoProviderTests.cs | 17 ++ MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsError.cs | 5 + .../ExecutionPlan/ExpressionExtensions.cs | 165 +++++++++--------- 3 files changed, 109 insertions(+), 78 deletions(-) diff --git a/MarkMpn.Sql4Cds.Engine.Tests/AdoProviderTests.cs b/MarkMpn.Sql4Cds.Engine.Tests/AdoProviderTests.cs index 2df8bcda..c08477ce 100644 --- a/MarkMpn.Sql4Cds.Engine.Tests/AdoProviderTests.cs +++ b/MarkMpn.Sql4Cds.Engine.Tests/AdoProviderTests.cs @@ -2122,5 +2122,22 @@ public void MissingCountParameter() } } } + + [TestMethod] + public void InvalidTypesForOperator() + { + using (var con = new Sql4CdsConnection(_localDataSources)) + { + try + { + con.Execute("SELECT * FROM account where name & ('1') > 1"); + Assert.Fail(); + } + catch (Sql4CdsException ex) + { + Assert.AreEqual(402, ex.Number); + } + } + } } } diff --git a/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsError.cs b/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsError.cs index 14b57687..529f59e3 100644 --- a/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsError.cs +++ b/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsError.cs @@ -740,6 +740,11 @@ internal static Sql4CdsError InvalidTypeForStatement(TSqlFragment fragment, stri return Create(15533, fragment, name); } + internal static Sql4CdsError IncompatibleDataTypesForOperator(TSqlFragment fragment, DataTypeReference type1, DataTypeReference type2, string op) + { + return Create(402, fragment, Collation.USEnglish.ToSqlString(GetTypeName(type1)), Collation.USEnglish.ToSqlString(GetTypeName(type2)), Collation.USEnglish.ToSqlString(op)); + } + private static string GetTypeName(DataTypeReference type) { if (type is SqlDataTypeReference sqlType) diff --git a/MarkMpn.Sql4Cds.Engine/ExecutionPlan/ExpressionExtensions.cs b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/ExpressionExtensions.cs index 8eb30656..8cabfeb1 100644 --- a/MarkMpn.Sql4Cds.Engine/ExecutionPlan/ExpressionExtensions.cs +++ b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/ExpressionExtensions.cs @@ -764,97 +764,106 @@ private static Expression ToExpression(Microsoft.SqlServer.TransactSql.ScriptDom sqlType = null; Expression expr; + var operatorString = ""; - switch (bin.BinaryExpressionType) + try { - case BinaryExpressionType.Add: - cacheKey = lhsCacheKey + " + " + rhsCacheKey; + switch (bin.BinaryExpressionType) + { + case BinaryExpressionType.Add: + operatorString = "+"; + + // Special case for SqlDateTime + if (lhs.Type == typeof(SqlDateTime) && rhs.Type == typeof(SqlDateTime)) + expr = Expr.Call(() => AddSqlDateTime(Expr.Arg(), Expr.Arg()), lhs, rhs); + else + expr = Expression.Add(lhs, rhs); + + // Special case for SqlString length & collation calculation + if (lhsSqlType is SqlDataTypeReferenceWithCollation lhsSql && + rhsSqlType is SqlDataTypeReferenceWithCollation rhsSql && + lhsSql.Parameters.Count == 1 && + rhsSql.Parameters.Count == 1) + { + int lhsLength; + int rhsLength; + + if (lhsSql.Parameters[0].LiteralType != LiteralType.Integer || + !Int32.TryParse(lhsSql.Parameters[0].Value, out lhsLength)) + lhsLength = 8000; + + if (rhsSql.Parameters[0].LiteralType != LiteralType.Integer || + !Int32.TryParse(rhsSql.Parameters[0].Value, out rhsLength)) + rhsLength = 8000; + + var length = lhsLength + rhsLength; + + if (!SqlDataTypeReferenceWithCollation.TryConvertCollation(lhsSql, rhsSql, bin, "add", out var collation, out var collationLabel, out var collationError)) + throw new NotSupportedQueryFragmentException(collationError); + + sqlType = new SqlDataTypeReferenceWithCollation + { + SqlDataTypeOption = ((SqlDataTypeReference)type).SqlDataTypeOption, + Parameters = { length <= 8000 ? (Literal)new IntegerLiteral { Value = length.ToString(CultureInfo.InvariantCulture) } : new MaxLiteral() }, + Collation = collation, + CollationLabel = collationLabel, + CollationConflictError = collationError + }; + } + break; - // Special case for SqlDateTime - if (lhs.Type == typeof(SqlDateTime) && rhs.Type == typeof(SqlDateTime)) - expr = Expr.Call(() => AddSqlDateTime(Expr.Arg(), Expr.Arg()), lhs, rhs); - else - expr = Expression.Add(lhs, rhs); + case BinaryExpressionType.Subtract: + operatorString = "-"; - // Special case for SqlString length & collation calculation - if (lhsSqlType is SqlDataTypeReferenceWithCollation lhsSql && - rhsSqlType is SqlDataTypeReferenceWithCollation rhsSql && - lhsSql.Parameters.Count == 1 && - rhsSql.Parameters.Count == 1) - { - int lhsLength; - int rhsLength; + // Special case for SqlDateTime + if (lhs.Type == typeof(SqlDateTime) && rhs.Type == typeof(SqlDateTime)) + expr = Expr.Call(() => SubtractSqlDateTime(Expr.Arg(), Expr.Arg()), lhs, rhs); + else + expr = Expression.Subtract(lhs, rhs); + break; - if (lhsSql.Parameters[0].LiteralType != LiteralType.Integer || - !Int32.TryParse(lhsSql.Parameters[0].Value, out lhsLength)) - lhsLength = 8000; + case BinaryExpressionType.Multiply: + operatorString = "*"; + expr = Expression.Multiply(lhs, rhs); + break; - if (rhsSql.Parameters[0].LiteralType != LiteralType.Integer || - !Int32.TryParse(rhsSql.Parameters[0].Value, out rhsLength)) - rhsLength = 8000; + case BinaryExpressionType.Divide: + operatorString = "/"; + expr = Expression.Divide(lhs, rhs); - var length = lhsLength + rhsLength; + expr = Expression.TryCatch(expr, Expression.Catch(typeof(DivideByZeroException), Expression.Throw(Expression.New(typeof(QueryExecutionException).GetConstructor(new[] { typeof(Sql4CdsError) }), Expr.Call(() => Sql4CdsError.DivideByZero())), expr.Type))); + break; - if (!SqlDataTypeReferenceWithCollation.TryConvertCollation(lhsSql, rhsSql, bin, "add", out var collation, out var collationLabel, out var collationError)) - throw new NotSupportedQueryFragmentException(collationError); + case BinaryExpressionType.Modulo: + operatorString = "%"; + expr = Expression.Modulo(lhs, rhs); + break; - sqlType = new SqlDataTypeReferenceWithCollation - { - SqlDataTypeOption = ((SqlDataTypeReference)type).SqlDataTypeOption, - Parameters = { length <= 8000 ? (Literal)new IntegerLiteral { Value = length.ToString(CultureInfo.InvariantCulture) } : new MaxLiteral() }, - Collation = collation, - CollationLabel = collationLabel, - CollationConflictError = collationError - }; - } - break; + case BinaryExpressionType.BitwiseAnd: + operatorString = "&"; + expr = Expression.And(lhs, rhs); + break; - case BinaryExpressionType.Subtract: - cacheKey = lhsCacheKey + " - " + rhsCacheKey; + case BinaryExpressionType.BitwiseOr: + operatorString = "|"; + expr = Expression.Or(lhs, rhs); + break; - // Special case for SqlDateTime - if (lhs.Type == typeof(SqlDateTime) && rhs.Type == typeof(SqlDateTime)) - expr = Expr.Call(() => SubtractSqlDateTime(Expr.Arg(), Expr.Arg()), lhs, rhs); - else - expr = Expression.Subtract(lhs, rhs); - break; - - case BinaryExpressionType.Multiply: - cacheKey = lhsCacheKey + " * " + rhsCacheKey; - expr = Expression.Multiply(lhs, rhs); - break; - - case BinaryExpressionType.Divide: - cacheKey = lhsCacheKey + " / " + rhsCacheKey; - expr = Expression.Divide(lhs, rhs); - - expr = Expression.TryCatch(expr, Expression.Catch(typeof(DivideByZeroException), Expression.Throw(Expression.New(typeof(QueryExecutionException).GetConstructor(new[] { typeof(Sql4CdsError) }), Expr.Call(() => Sql4CdsError.DivideByZero())), expr.Type))); - //expr = Expression.Invoke(Expression.Lambda(expr)); - break; - - case BinaryExpressionType.Modulo: - cacheKey = lhsCacheKey + " % " + rhsCacheKey; - expr = Expression.Modulo(lhs, rhs); - break; - - case BinaryExpressionType.BitwiseAnd: - cacheKey = lhsCacheKey + " & " + rhsCacheKey; - expr = Expression.And(lhs, rhs); - break; - - case BinaryExpressionType.BitwiseOr: - cacheKey = lhsCacheKey + " | " + rhsCacheKey; - expr = Expression.Or(lhs, rhs); - break; - - case BinaryExpressionType.BitwiseXor: - cacheKey = lhsCacheKey + " ^ " + rhsCacheKey; - expr = Expression.ExclusiveOr(lhs, rhs); - break; + case BinaryExpressionType.BitwiseXor: + operatorString = "^"; + expr = Expression.ExclusiveOr(lhs, rhs); + break; - default: - throw new NotSupportedQueryFragmentException(Sql4CdsError.SyntaxError(bin)) { Suggestion = "Unknown operator" }; + default: + throw new NotSupportedQueryFragmentException(Sql4CdsError.SyntaxError(bin)) { Suggestion = "Unknown operator" }; + } } + catch (InvalidOperationException) + { + throw new NotSupportedQueryFragmentException(Sql4CdsError.IncompatibleDataTypesForOperator(bin, lhsSqlType, rhsSqlType, operatorString)); + } + + cacheKey = $"{lhsCacheKey} {operatorString} {rhsCacheKey}"; if (sqlType == null && expr.Type == typeof(SqlDecimal)) sqlType = type; From a57cf8164f1120d7c7991978e1c3a609021979a0 Mon Sep 17 00:00:00 2001 From: Mark Carrington <31017244+MarkMpn@users.noreply.github.com> Date: Tue, 25 Jun 2024 22:30:36 +0100 Subject: [PATCH 10/25] Improved error reporting when using wildcard instead of column name --- .../AdoProviderTests.cs | 17 +++++++++++++++++ .../ExecutionPlan/ExpressionExtensions.cs | 4 ++++ 2 files changed, 21 insertions(+) diff --git a/MarkMpn.Sql4Cds.Engine.Tests/AdoProviderTests.cs b/MarkMpn.Sql4Cds.Engine.Tests/AdoProviderTests.cs index c08477ce..50cd7536 100644 --- a/MarkMpn.Sql4Cds.Engine.Tests/AdoProviderTests.cs +++ b/MarkMpn.Sql4Cds.Engine.Tests/AdoProviderTests.cs @@ -2139,5 +2139,22 @@ public void InvalidTypesForOperator() } } } + + [TestMethod] + public void WildcardColumnAsFunctionParameterIsSyntaxError() + { + using (var con = new Sql4CdsConnection(_localDataSources)) + { + try + { + con.Execute("SELECT SIZE(*) FROM account"); + Assert.Fail(); + } + catch (Sql4CdsException ex) + { + Assert.AreEqual(102, ex.Number); + } + } + } } } diff --git a/MarkMpn.Sql4Cds.Engine/ExecutionPlan/ExpressionExtensions.cs b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/ExpressionExtensions.cs index 8cabfeb1..212d9fe1 100644 --- a/MarkMpn.Sql4Cds.Engine/ExecutionPlan/ExpressionExtensions.cs +++ b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/ExpressionExtensions.cs @@ -253,6 +253,10 @@ private static Expression InvokeSubExpression(this TParent pare private static Expression ToExpression(ColumnReferenceExpression col, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, bool createExpression, out DataTypeReference sqlType, out string cacheKey) { + // Wildcard columns shouldn't appear in an expression + if (col.ColumnType == ColumnType.Wildcard) + throw new NotSupportedQueryFragmentException(Sql4CdsError.SyntaxError(col)); + var name = col.GetColumnName(); if (context.Schema == null || !context.Schema.ContainsColumn(name, out var normalizedName)) From b029f125129ad492279e8f450dd0da06a22d3a96 Mon Sep 17 00:00:00 2001 From: Mark Carrington <31017244+MarkMpn@users.noreply.github.com> Date: Sat, 29 Jun 2024 16:23:40 +0100 Subject: [PATCH 11/25] Added initial export to CSV/Excel for XTB tool --- .../Contracts/DbCellValue.cs | 13 +- .../Contracts/DbColumnWrapper.cs | 101 +- .../Contracts/SaveResultsRequest.cs | 194 ++++ .../DataStorage/FileStreamReadResult.cs | 44 + .../DataStorage/IFileStreamFactory.cs | 25 + .../DataStorage/IFileStreamReader.cs | 19 + .../DataStorage/IFileStreamWriter.cs | 23 + .../DataStorage/SaveAsCsvFileStreamFactory.cs | 78 ++ .../DataStorage/SaveAsCsvFileStreamWriter.cs | 141 +++ .../SaveAsExcelFileStreamFactory.cs | 78 ++ .../SaveAsExcelFileStreamWriter.cs | 219 ++++ .../SaveAsExcelFileStreamWriterHelper.cs | 964 ++++++++++++++++++ .../SaveAsJsonFileStreamFactory.cs | 74 ++ .../DataStorage/SaveAsJsonFileStreamWriter.cs | 113 ++ .../SaveAsMarkdownFileStreamFactory.cs | 63 ++ .../SaveAsMarkdownFileStreamWriter.cs | 103 ++ .../DataStorage/SaveAsWriterBase.cs | 180 ++++ .../DataStorage/SaveAsXmlFileStreamFactory.cs | 76 ++ .../DataStorage/SaveAsXmlFileStreamWriter.cs | 144 +++ .../ServiceBufferFileStreamFactory.cs | 66 ++ .../ServiceBufferFileStreamReader.cs | 645 ++++++++++++ .../ServiceBufferFileStreamWriter.cs | 580 +++++++++++ .../DataStorage/StorageDataReader.cs | 343 +++++++ .../MarkMpn.Sql4Cds.Export.csproj | 18 + MarkMpn.Sql4Cds.Export/README.md | 2 + MarkMpn.Sql4Cds.Export/SR.cs | 21 + MarkMpn.Sql4Cds.Export/Utility/Extensions.cs | 99 ++ MarkMpn.Sql4Cds.Export/Utility/FileUtils.cs | 144 +++ MarkMpn.Sql4Cds.Export/Utility/Validate.cs | 158 +++ MarkMpn.Sql4Cds.Export/ValueFormatter.cs | 68 ++ .../MarkMpn.Sql4Cds.LanguageServer.csproj | 1 + .../Contracts/ResultSetSubset.cs | 4 +- .../Contracts/ResultSetSummary.cs | 1 + .../QueryExecution/QueryExecutionHandler.cs | 60 +- .../MarkMpn.Sql4Cds.XTB.csproj | 10 + .../SqlQueryControl.Designer.cs | 30 +- MarkMpn.Sql4Cds.XTB/SqlQueryControl.cs | 276 +++-- MarkMpn.Sql4Cds.XTB/SqlQueryControl.resx | 2 +- MarkMpn.Sql4Cds.sln | 14 + 39 files changed, 5049 insertions(+), 145 deletions(-) rename {MarkMpn.Sql4Cds.LanguageServer/QueryExecution => MarkMpn.Sql4Cds.Export}/Contracts/DbCellValue.cs (81%) rename {MarkMpn.Sql4Cds.LanguageServer/QueryExecution => MarkMpn.Sql4Cds.Export}/Contracts/DbColumnWrapper.cs (75%) create mode 100644 MarkMpn.Sql4Cds.Export/Contracts/SaveResultsRequest.cs create mode 100644 MarkMpn.Sql4Cds.Export/DataStorage/FileStreamReadResult.cs create mode 100644 MarkMpn.Sql4Cds.Export/DataStorage/IFileStreamFactory.cs create mode 100644 MarkMpn.Sql4Cds.Export/DataStorage/IFileStreamReader.cs create mode 100644 MarkMpn.Sql4Cds.Export/DataStorage/IFileStreamWriter.cs create mode 100644 MarkMpn.Sql4Cds.Export/DataStorage/SaveAsCsvFileStreamFactory.cs create mode 100644 MarkMpn.Sql4Cds.Export/DataStorage/SaveAsCsvFileStreamWriter.cs create mode 100644 MarkMpn.Sql4Cds.Export/DataStorage/SaveAsExcelFileStreamFactory.cs create mode 100644 MarkMpn.Sql4Cds.Export/DataStorage/SaveAsExcelFileStreamWriter.cs create mode 100644 MarkMpn.Sql4Cds.Export/DataStorage/SaveAsExcelFileStreamWriterHelper.cs create mode 100644 MarkMpn.Sql4Cds.Export/DataStorage/SaveAsJsonFileStreamFactory.cs create mode 100644 MarkMpn.Sql4Cds.Export/DataStorage/SaveAsJsonFileStreamWriter.cs create mode 100644 MarkMpn.Sql4Cds.Export/DataStorage/SaveAsMarkdownFileStreamFactory.cs create mode 100644 MarkMpn.Sql4Cds.Export/DataStorage/SaveAsMarkdownFileStreamWriter.cs create mode 100644 MarkMpn.Sql4Cds.Export/DataStorage/SaveAsWriterBase.cs create mode 100644 MarkMpn.Sql4Cds.Export/DataStorage/SaveAsXmlFileStreamFactory.cs create mode 100644 MarkMpn.Sql4Cds.Export/DataStorage/SaveAsXmlFileStreamWriter.cs create mode 100644 MarkMpn.Sql4Cds.Export/DataStorage/ServiceBufferFileStreamFactory.cs create mode 100644 MarkMpn.Sql4Cds.Export/DataStorage/ServiceBufferFileStreamReader.cs create mode 100644 MarkMpn.Sql4Cds.Export/DataStorage/ServiceBufferFileStreamWriter.cs create mode 100644 MarkMpn.Sql4Cds.Export/DataStorage/StorageDataReader.cs create mode 100644 MarkMpn.Sql4Cds.Export/MarkMpn.Sql4Cds.Export.csproj create mode 100644 MarkMpn.Sql4Cds.Export/README.md create mode 100644 MarkMpn.Sql4Cds.Export/SR.cs create mode 100644 MarkMpn.Sql4Cds.Export/Utility/Extensions.cs create mode 100644 MarkMpn.Sql4Cds.Export/Utility/FileUtils.cs create mode 100644 MarkMpn.Sql4Cds.Export/Utility/Validate.cs create mode 100644 MarkMpn.Sql4Cds.Export/ValueFormatter.cs diff --git a/MarkMpn.Sql4Cds.LanguageServer/QueryExecution/Contracts/DbCellValue.cs b/MarkMpn.Sql4Cds.Export/Contracts/DbCellValue.cs similarity index 81% rename from MarkMpn.Sql4Cds.LanguageServer/QueryExecution/Contracts/DbCellValue.cs rename to MarkMpn.Sql4Cds.Export/Contracts/DbCellValue.cs index c4233040..c3077b34 100644 --- a/MarkMpn.Sql4Cds.LanguageServer/QueryExecution/Contracts/DbCellValue.cs +++ b/MarkMpn.Sql4Cds.Export/Contracts/DbCellValue.cs @@ -1,4 +1,11 @@ -namespace MarkMpn.Sql4Cds.LanguageServer.QueryExecution.Contracts +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.SqlTools.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts { /// /// Class used for internally passing results from a cell around. @@ -23,7 +30,7 @@ public class DbCellValue /// /// The raw object for the cell, for use internally /// - internal object RawObject { get; set; } + public object RawObject { get; set; } /// /// The internal ID for the row. Should be used when directly referencing the row for edit @@ -37,6 +44,8 @@ public class DbCellValue /// The DbCellValue (or child) that will receive the values public virtual void CopyTo(DbCellValue other) { + Validate.IsNotNull(nameof(other), other); + other.DisplayValue = DisplayValue; other.InvariantCultureDisplayValue = InvariantCultureDisplayValue; other.IsNull = IsNull; diff --git a/MarkMpn.Sql4Cds.LanguageServer/QueryExecution/Contracts/DbColumnWrapper.cs b/MarkMpn.Sql4Cds.Export/Contracts/DbColumnWrapper.cs similarity index 75% rename from MarkMpn.Sql4Cds.LanguageServer/QueryExecution/Contracts/DbColumnWrapper.cs rename to MarkMpn.Sql4Cds.Export/Contracts/DbColumnWrapper.cs index ea1a5dd0..a36ce8a3 100644 --- a/MarkMpn.Sql4Cds.LanguageServer/QueryExecution/Contracts/DbColumnWrapper.cs +++ b/MarkMpn.Sql4Cds.Export/Contracts/DbColumnWrapper.cs @@ -1,11 +1,18 @@ -using System; +// +// 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; using System.Data.Common; using System.Data.SqlTypes; using System.Diagnostics; +using Microsoft.SqlTools.Utility; -namespace MarkMpn.Sql4Cds.LanguageServer.QueryExecution.Contracts +namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts { /// /// Wrapper around a DbColumn, which provides extra functionality, but can be used as a @@ -114,20 +121,19 @@ public DbColumnWrapper(DbColumn column) } } - public DbColumnWrapper(ColumnInfo columnInfo) + public DbColumnWrapper(string name, string dataTypeName, int? numericScale) { - DataTypeName = columnInfo.DataTypeName.ToLowerInvariant(); + DataTypeName = dataTypeName.ToLowerInvariant(); DetermineSqlDbType(); - DataType = TypeConvertor.ToNetType(SqlDbType); - if (DataType == typeof(string)) + DataType = TypeConvertor.ToNetType(this.SqlDbType); + if (DataType == typeof(String)) { - ColumnSize = int.MaxValue; + this.ColumnSize = int.MaxValue; } - AddNameAndDataFields(columnInfo.Name); - NumericScale = columnInfo.NumericScale; + AddNameAndDataFields(name); + NumericScale = numericScale; } - /// /// Default constructor, used for deserializing JSON RPC only /// @@ -206,9 +212,9 @@ public DbColumnWrapper() /// Logic taken from SSDT determination of updatable columns /// Special treatment for HierarchyId since we are using an Expression for HierarchyId column and expression column is readonly. /// - public bool IsUpdatable => IsAutoIncrement != true && - IsReadOnly != true && - !IsSqlXmlType || IsHierarchyId; + public bool IsUpdatable => (!IsAutoIncrement.HasTrue() && + !IsReadOnly.HasTrue() && + !IsSqlXmlType) || IsHierarchyId; #endregion @@ -248,7 +254,7 @@ private void AddNameAndDataFields(string columnName) { // We want the display name for the column to always exist ColumnName = string.IsNullOrEmpty(columnName) - ? null//SR.QueryServiceColumnNull + ? SR.QueryServiceColumnNull : columnName; switch (DataTypeName) @@ -317,4 +323,71 @@ private void AddNameAndDataFields(string columnName) } } } + + + + /// + /// Convert a base data type to another base data type + /// + public sealed class TypeConvertor + { + private static Dictionary _typeMap = new Dictionary(); + + static TypeConvertor() + { + _typeMap[SqlDbType.BigInt] = typeof(Int64); + _typeMap[SqlDbType.Binary] = typeof(Byte); + _typeMap[SqlDbType.Bit] = typeof(Boolean); + _typeMap[SqlDbType.Char] = typeof(String); + _typeMap[SqlDbType.DateTime] = typeof(DateTime); + _typeMap[SqlDbType.Decimal] = typeof(Decimal); + _typeMap[SqlDbType.Float] = typeof(Double); + _typeMap[SqlDbType.Image] = typeof(Byte[]); + _typeMap[SqlDbType.Int] = typeof(Int32); + _typeMap[SqlDbType.Money] = typeof(Decimal); + _typeMap[SqlDbType.NChar] = typeof(String); + _typeMap[SqlDbType.NChar] = typeof(String); + _typeMap[SqlDbType.NChar] = typeof(String); + _typeMap[SqlDbType.NText] = typeof(String); + _typeMap[SqlDbType.NVarChar] = typeof(String); + _typeMap[SqlDbType.Real] = typeof(Single); + _typeMap[SqlDbType.UniqueIdentifier] = typeof(Guid); + _typeMap[SqlDbType.SmallDateTime] = typeof(DateTime); + _typeMap[SqlDbType.SmallInt] = typeof(Int16); + _typeMap[SqlDbType.SmallMoney] = typeof(Decimal); + _typeMap[SqlDbType.Text] = typeof(String); + _typeMap[SqlDbType.Timestamp] = typeof(Byte[]); + _typeMap[SqlDbType.TinyInt] = typeof(Byte); + _typeMap[SqlDbType.VarBinary] = typeof(Byte[]); + _typeMap[SqlDbType.VarChar] = typeof(String); + _typeMap[SqlDbType.Variant] = typeof(Object); + // Note: treating as string + _typeMap[SqlDbType.Xml] = typeof(String); + _typeMap[SqlDbType.TinyInt] = typeof(Byte); + _typeMap[SqlDbType.TinyInt] = typeof(Byte); + _typeMap[SqlDbType.TinyInt] = typeof(Byte); + _typeMap[SqlDbType.TinyInt] = typeof(Byte); + } + + private TypeConvertor() + { + + } + + + /// + /// Convert TSQL type to .Net data type + /// + /// + /// + public static Type ToNetType(SqlDbType sqlDbType) + { + Type netType; + if (!_typeMap.TryGetValue(sqlDbType, out netType)) + { + netType = typeof(String); + } + return netType; + } + } } diff --git a/MarkMpn.Sql4Cds.Export/Contracts/SaveResultsRequest.cs b/MarkMpn.Sql4Cds.Export/Contracts/SaveResultsRequest.cs new file mode 100644 index 00000000..1663e791 --- /dev/null +++ b/MarkMpn.Sql4Cds.Export/Contracts/SaveResultsRequest.cs @@ -0,0 +1,194 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + + +namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts +{ + /// + /// Parameters for the save results request + /// + public class SaveResultsRequestParams + { + /// + /// The path of the file to save results in + /// + public string FilePath { get; set; } + + /// + /// Index of the batch to get the results from + /// + public int BatchIndex { get; set; } + + /// + /// Index of the result set to get the results from + /// + public int ResultSetIndex { get; set; } + + /// + /// URI for the editor that called save results + /// + public string OwnerUri { get; set; } + + /// + /// Start index of the selected rows (inclusive) + /// + public int? RowStartIndex { get; set; } + + /// + /// End index of the selected rows (inclusive) + /// + public int? RowEndIndex { get; set; } + + /// + /// Start index of the selected columns (inclusive) + /// + /// + public int? ColumnStartIndex { get; set; } + + /// + /// End index of the selected columns (inclusive) + /// + /// + public int? ColumnEndIndex { get; set; } + + /// + /// Check if request is a subset of result set or whole result set + /// + /// + internal bool IsSaveSelection + { + get + { + return ColumnStartIndex.HasValue && ColumnEndIndex.HasValue + && RowStartIndex.HasValue && RowEndIndex.HasValue; + } + } + } + + /// + /// Parameters to save results as CSV + /// + public class SaveResultsAsCsvRequestParams: SaveResultsRequestParams + { + /// + /// Include headers of columns in CSV + /// + public bool IncludeHeaders { get; set; } + + /// + /// Delimiter for separating data items in CSV + /// + public string Delimiter { get; set; } + + /// + /// either CR, CRLF or LF to separate rows in CSV + /// + public string LineSeperator { get; set; } + + /// + /// Text identifier for alphanumeric columns in CSV + /// + public string TextIdentifier { get; set; } + + /// + /// Encoding of the CSV file + /// + public string Encoding { get; set; } + + /// + /// Maximum number of characters to store + /// + public int MaxCharsToStore { get; set; } + } + + /// + /// Parameters to save results as Excel + /// + public class SaveResultsAsExcelRequestParams : SaveResultsRequestParams + { + /// + /// Include headers of columns in Excel + /// + public bool IncludeHeaders { get; set; } + + /// + /// Freeze header row in Excel + /// + public bool FreezeHeaderRow { get; set; } + + /// + /// Bold header row in Excel + /// + public bool BoldHeaderRow { get; set; } + + /// + /// Enable auto filter on header row in Excel + /// + public bool AutoFilterHeaderRow { get; set; } + + /// + /// Auto size columns in Excel + /// + public bool AutoSizeColumns { get; set; } + } + + /// + /// Parameters to save results as JSON + /// + public class SaveResultsAsJsonRequestParams: SaveResultsRequestParams + { + //TODO: define config for save as JSON + } + + /// + /// Parameters for saving results as a Markdown table + /// + public class SaveResultsAsMarkdownRequestParams : SaveResultsRequestParams + { + /// + /// Encoding of the CSV file + /// + public string Encoding { get; set; } + + /// + /// Whether to include column names as header for the table. + /// + public bool IncludeHeaders { get; set; } + + /// + /// Character sequence to separate a each row in the table. Should be either CR, CRLF, or + /// LF. If not provided, defaults to the system default line ending sequence. + /// + public string LineSeparator { get; set; } + } + + /// + /// Parameters to save results as XML + /// + public class SaveResultsAsXmlRequestParams: SaveResultsRequestParams + { + /// + /// Formatting of the XML file + /// + public bool Formatted { get; set; } + + /// + /// Encoding of the XML file + /// + public string Encoding { get; set; } + } + + /// + /// Parameters for the save results result + /// + public class SaveResultRequestResult + { + /// + /// Error messages for saving to file. + /// + public string Messages { get; set; } + } + +} \ No newline at end of file diff --git a/MarkMpn.Sql4Cds.Export/DataStorage/FileStreamReadResult.cs b/MarkMpn.Sql4Cds.Export/DataStorage/FileStreamReadResult.cs new file mode 100644 index 00000000..0939c9d4 --- /dev/null +++ b/MarkMpn.Sql4Cds.Export/DataStorage/FileStreamReadResult.cs @@ -0,0 +1,44 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage +{ + /// + /// Represents a value returned from a read from a file stream. This is used to eliminate ref + /// parameters used in the read methods. + /// + public struct FileStreamReadResult + { + /// + /// The total length in bytes of the value, (including the bytes used to store the length + /// of the value) + /// + /// + /// Cell values are stored such that the length of the value is stored first, then the + /// value itself is stored. Eg, a string may be stored as 0x03 0x6C 0x6F 0x6C. Under this + /// system, the value would be "lol", the length would be 3, and the total length would be + /// 4 bytes. + /// + public int TotalLength { get; set; } + + /// + /// Value of the cell + /// + public DbCellValue Value { get; set; } + + /// + /// Constructs a new FileStreamReadResult + /// + /// The value of the result, ready for consumption by a client + /// The number of bytes for the used to store the value's length and values + public FileStreamReadResult(DbCellValue value, int totalLength) + { + Value = value; + TotalLength = totalLength; + } + } +} diff --git a/MarkMpn.Sql4Cds.Export/DataStorage/IFileStreamFactory.cs b/MarkMpn.Sql4Cds.Export/DataStorage/IFileStreamFactory.cs new file mode 100644 index 00000000..63e34637 --- /dev/null +++ b/MarkMpn.Sql4Cds.Export/DataStorage/IFileStreamFactory.cs @@ -0,0 +1,25 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage +{ + /// + /// Interface for a factory that creates filesystem readers/writers + /// + public interface IFileStreamFactory + { + string CreateFile(); + + IFileStreamReader GetReader(string fileName); + + IFileStreamWriter GetWriter(string fileName, IReadOnlyList columns = null); + + void DisposeFile(string fileName); + + } +} diff --git a/MarkMpn.Sql4Cds.Export/DataStorage/IFileStreamReader.cs b/MarkMpn.Sql4Cds.Export/DataStorage/IFileStreamReader.cs new file mode 100644 index 00000000..1ecc5c0b --- /dev/null +++ b/MarkMpn.Sql4Cds.Export/DataStorage/IFileStreamReader.cs @@ -0,0 +1,19 @@ +// +// 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 Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage +{ + /// + /// Interface for a object that reads from the filesystem + /// + public interface IFileStreamReader : IDisposable + { + IList ReadRow(long offset, long rowId, IEnumerable columns); + } +} diff --git a/MarkMpn.Sql4Cds.Export/DataStorage/IFileStreamWriter.cs b/MarkMpn.Sql4Cds.Export/DataStorage/IFileStreamWriter.cs new file mode 100644 index 00000000..51399534 --- /dev/null +++ b/MarkMpn.Sql4Cds.Export/DataStorage/IFileStreamWriter.cs @@ -0,0 +1,23 @@ +// +// 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 Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; + + +namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage +{ + /// + /// Interface for a object that writes to a filesystem wrapper + /// + public interface IFileStreamWriter : IDisposable + { + int WriteRow(StorageDataReader dataReader); + void WriteRow(IList row, IReadOnlyList columns); + void Seek(long offset); + void FlushBuffer(); + } +} diff --git a/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsCsvFileStreamFactory.cs b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsCsvFileStreamFactory.cs new file mode 100644 index 00000000..674599ea --- /dev/null +++ b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsCsvFileStreamFactory.cs @@ -0,0 +1,78 @@ +// +// 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.IO; +using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using Microsoft.SqlTools.ServiceLayer.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage +{ + /// + /// Factory for creating a reader/writer pair that will read from the temporary buffer file + /// and output to a CSV file. + /// + public class SaveAsCsvFileStreamFactory : IFileStreamFactory + { + #region Properties + + /// + /// Parameters for the save as CSV request + /// + public SaveResultsAsCsvRequestParams SaveRequestParams { get; set; } + + #endregion + + /// + /// File names are not meant to be created with this factory. + /// + /// Thrown all times + [Obsolete] + public string CreateFile() + { + throw new NotImplementedException(); + } + + /// + /// Returns a new service buffer reader for reading results back in from the temporary buffer files, file share is ReadWrite to allow concurrent reads/writes to the file. + /// + /// Path to the temp buffer file + /// Stream reader + public IFileStreamReader GetReader(string fileName) + { + return new ServiceBufferFileStreamReader( + new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite) + ); + } + + /// + /// Returns a new CSV writer for writing results to a CSV file, file share is ReadWrite to allow concurrent reads/writes to the file. + /// + /// Path to the CSV output file + /// + /// The entire list of columns for the result set. They will be filtered down as per the + /// request params. + /// + /// Stream writer + public IFileStreamWriter GetWriter(string fileName, IReadOnlyList columns) + { + return new SaveAsCsvFileStreamWriter( + new FileStream(fileName, FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite), + SaveRequestParams, + columns + ); + } + + /// + /// Safely deletes the file + /// + /// Path to the file to delete + public void DisposeFile(string fileName) + { + FileUtilities.SafeFileDelete(fileName); + } + } +} diff --git a/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsCsvFileStreamWriter.cs b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsCsvFileStreamWriter.cs new file mode 100644 index 00000000..1a12375e --- /dev/null +++ b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsCsvFileStreamWriter.cs @@ -0,0 +1,141 @@ +// +// 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.IO; +using System.Linq; +using System.Text; +using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage +{ + /// + /// Writer for writing rows of results to a CSV file + /// + public class SaveAsCsvFileStreamWriter : SaveAsStreamWriter + { + + #region Member Variables + + private readonly char delimiter; + private readonly Encoding encoding; + private readonly string lineSeparator; + private readonly char textIdentifier; + private readonly string textIdentifierString; + + #endregion + + /// + /// Constructor, stores the CSV specific request params locally, chains into the base + /// constructor + /// + /// FileStream to access the CSV file output + /// CSV save as request parameters + /// + /// The entire list of columns for the result set. They will be filtered down as per the + /// request params. + /// + public SaveAsCsvFileStreamWriter(Stream stream, SaveResultsAsCsvRequestParams requestParams, IReadOnlyList columns) + : base(stream, requestParams, columns) + { + // Parse the config + delimiter = ','; + if (!string.IsNullOrEmpty(requestParams.Delimiter)) + { + delimiter = requestParams.Delimiter[0]; + } + + lineSeparator = Environment.NewLine; + if (!string.IsNullOrEmpty(requestParams.LineSeperator)) + { + lineSeparator = requestParams.LineSeperator; + } + + textIdentifier = '"'; + if (!string.IsNullOrEmpty(requestParams.TextIdentifier)) + { + textIdentifier = requestParams.TextIdentifier[0]; + } + textIdentifierString = textIdentifier.ToString(); + + encoding = ParseEncoding(requestParams.Encoding, Encoding.UTF8); + + // Output the header if the user requested it + if (requestParams.IncludeHeaders) + { + // Build the string + var selectedColumns = columns.Skip(ColumnStartIndex) + .Take(ColumnCount) + .Select(c => EncodeCsvField(c.ColumnName) ?? string.Empty); + + string headerLine = string.Join(delimiter.ToString(), selectedColumns); + + // Encode it and write it out + byte[] headerBytes = encoding.GetBytes(headerLine + lineSeparator); + FileStream.Write(headerBytes, 0, headerBytes.Length); + } + } + + /// + /// Writes a row of data as a CSV row. If this is the first row and the user has requested + /// it, the headers for the column will be emitted as well. + /// + /// The data of the row to output to the file + /// The columns for the row to output + public override void WriteRow(IList row, IReadOnlyList columns) + { + // Build the string for the row + var selectedCells = row.Skip(ColumnStartIndex) + .Take(ColumnCount) + .Select(c => EncodeCsvField(c.DisplayValue)); + string rowLine = string.Join(delimiter.ToString(), selectedCells); + + // Encode it and write it out + byte[] rowBytes = encoding.GetBytes(rowLine + lineSeparator); + FileStream.Write(rowBytes, 0, rowBytes.Length); + } + + /// + /// Encodes a single field for inserting into a CSV record. The following rules are applied: + /// + /// All double quotes (") are replaced with a pair of consecutive double quotes + /// + /// The entire field is also surrounded by a pair of double quotes if any of the following conditions are met: + /// + /// The field begins or ends with a space + /// The field begins or ends with a tab + /// The field contains the delimiter string + /// The field contains the '\n' character + /// The field contains the '\r' character + /// The field contains the '"' character + /// + /// + /// The field to encode + /// The CSV encoded version of the original field + internal string EncodeCsvField(string field) + { + // Special case for nulls + if (field == null) + { + return "NULL"; + } + + // Replace all quotes in the original field with double quotes + string ret = field.Replace(textIdentifierString, textIdentifierString + textIdentifierString); + + // Whether this field has special characters which require it to be embedded in quotes + bool embedInQuotes = field.IndexOfAny(new[] { delimiter, '\r', '\n', textIdentifier }) >= 0 // Contains special characters + || field.StartsWith(" ") || field.EndsWith(" ") // Start/Ends with space + || field.StartsWith("\t") || field.EndsWith("\t"); // Starts/Ends with tab + if (embedInQuotes) + { + ret = $"{textIdentifier}{ret}{textIdentifier}"; + } + + return ret; + } + } +} \ No newline at end of file diff --git a/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsExcelFileStreamFactory.cs b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsExcelFileStreamFactory.cs new file mode 100644 index 00000000..6184de15 --- /dev/null +++ b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsExcelFileStreamFactory.cs @@ -0,0 +1,78 @@ +// +// 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.IO; +using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using Microsoft.SqlTools.ServiceLayer.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage +{ + /// + /// Factory for creating a reader/writer pair that will read from the temporary buffer file + /// and output to a Excel file. + /// + public class SaveAsExcelFileStreamFactory : IFileStreamFactory + { + #region Properties + + /// + /// Parameters for the save as Excel request + /// + public SaveResultsAsExcelRequestParams SaveRequestParams { get; set; } + + #endregion + + /// + /// File names are not meant to be created with this factory. + /// + /// Thrown all times + [Obsolete] + public string CreateFile() + { + throw new NotImplementedException(); + } + + /// + /// Returns a new service buffer reader for reading results back in from the temporary buffer files, file share is ReadWrite to allow concurrent reads/writes to the file. + /// + /// Path to the temp buffer file + /// Stream reader + public IFileStreamReader GetReader(string fileName) + { + return new ServiceBufferFileStreamReader( + new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite) + ); + } + + /// + /// Returns a new Excel writer for writing results to a Excel file, file share is ReadWrite to allow concurrent reads/writes to the file. + /// + /// Path to the Excel output file + /// + /// The entire list of columns for the result set. They will be filtered down as per the + /// request params. + /// + /// Stream writer + public IFileStreamWriter GetWriter(string fileName, IReadOnlyList columns) + { + return new SaveAsExcelFileStreamWriter( + new FileStream(fileName, FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite), + SaveRequestParams, + columns + ); + } + + /// + /// Safely deletes the file + /// + /// Path to the file to delete + public void DisposeFile(string fileName) + { + FileUtilities.SafeFileDelete(fileName); + } + } +} diff --git a/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsExcelFileStreamWriter.cs b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsExcelFileStreamWriter.cs new file mode 100644 index 00000000..8782f042 --- /dev/null +++ b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsExcelFileStreamWriter.cs @@ -0,0 +1,219 @@ +// +// 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.IO; +using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using SkiaSharp; + +namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage +{ + /// + /// Writer for writing rows of results to a Excel file + /// + 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 + + private readonly SaveResultsAsExcelRequestParams saveParams; + private readonly float[] columnWidths; + private readonly int columnEndIndex; + private readonly int columnStartIndex; + private readonly SaveAsExcelFileStreamWriterHelper helper; + + private bool headerWritten; + private SaveAsExcelFileStreamWriterHelper.ExcelSheet sheet; + + private SKPaint paint; + + #endregion + + /// + /// Constructor, stores the Excel specific request params locally, chains into the base + /// constructor + /// + /// FileStream to access the Excel file output + /// Excel save as request parameters + /// + /// The entire list of columns for the result set. They will be filtered down as per the + /// request params. + /// + public SaveAsExcelFileStreamWriter(Stream stream, SaveResultsAsExcelRequestParams requestParams, IReadOnlyList columns) + : base(stream, requestParams, columns) + { + saveParams = requestParams; + helper = new SaveAsExcelFileStreamWriterHelper(stream); + + // 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; + } + } + } + } + } + + /// + /// Excel supports specifying column widths so measure if the user wants the columns automatically sized + /// + public override bool ShouldMeasureRowColumns => saveParams.AutoSizeColumns; + + /// + /// Measures each column of a row of data and stores updates the maximum width of the column if needed + /// + /// The row of data to measure + public override void MeasureRowColumns(IList 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); + } + } + + /// + /// Writes a row of data as a Excel row. If this is the first row and the user has requested + /// it, the headers for the column will be emitted as well. + /// + /// The data of the row to output to the file + /// + /// The entire list of columns for the result set. They will be filtered down as per the + /// request params. + /// + public override void WriteRow(IList row, IReadOnlyList 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 + if (saveParams.IncludeHeaders && !headerWritten) + { + sheet.AddRow(); + for (int i = ColumnStartIndex; i <= ColumnEndIndex; i++) + { + // Add the header text and bold if requested + sheet.AddCell(columns[i].ColumnName, saveParams.BoldHeaderRow); + } + headerWritten = true; + } + + sheet.AddRow(); + for (int i = ColumnStartIndex; i <= ColumnEndIndex; i++) + { + sheet.AddCell(row[i]); + } + } + + private bool disposed; + protected override void Dispose(bool disposing) + { + if (disposed) + return; + + sheet.Dispose(); + helper.Dispose(); + + disposed = true; + base.Dispose(disposing); + } + + } +} \ No newline at end of file diff --git a/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsExcelFileStreamWriterHelper.cs b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsExcelFileStreamWriterHelper.cs new file mode 100644 index 00000000..aa085c4f --- /dev/null +++ b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsExcelFileStreamWriterHelper.cs @@ -0,0 +1,964 @@ +// +// 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.Globalization; +using System.IO; +using System.IO.Compression; +using System.Xml; +using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage +{ + // A xlsx file is a zip with specific folder structure. + // http://www.ecma-international.org/publications/standards/Ecma-376.htm + + // The page number in the comments are based on + // ECMA-376, Fifth Edition, Part 1 - Fundamentals And Markup Language Reference + + // Page 75, SpreadsheetML package structure + // |- [Content_Types].xml + // |- _rels + // |- .rels + // |- xl + // |- workbook.xml + // |- styles.xml + // |- _rels + // |- workbook.xml.rels + // |- worksheets + // |- sheet1.xml + + /// + /// A helper class for write xlsx file base on ECMA-376. It tries to be minimal, + /// both in implementation and runtime allocation. + /// + /// + /// This sample shows how to use the class + /// + /// public class TestClass + /// { + /// public static int Main() + /// { + /// using (Stream stream = File.Create("test.xlsx")) + /// using (var helper = new SaveAsExcelFileStreamWriterHelper(stream, false)) + /// using (var sheet = helper.AddSheet()) + /// { + /// sheet.AddRow(); + /// sheet.AddCell("string"); + /// } + /// } + /// } + /// + /// + + internal sealed class SaveAsExcelFileStreamWriterHelper : IDisposable + { + /// + /// Present a Excel sheet + /// + public sealed class ExcelSheet : IDisposable + { + // The excel epoch is 1/1/1900, but it has 1/0/1900 and 2/29/1900 + // which is equal to set the epoch back two days to 12/30/1899 + // new DateTime(1899,12,30).Ticks + private const long ExcelEpochTick = 599264352000000000L; + + // Excel can not use date before 1/0/1900 and + // date before 3/1/1900 is wrong, off by 1 because of 2/29/1900 + // thus, for any date before 3/1/1900, use string for date + // new DateTime(1900,3,1).Ticks + private const long ExcelDateCutoffTick = 599317056000000000L; + + // new TimeSpan(24,0,0).Ticks + private const long TicksPerDay = 864000000000L; + + // Digit pixel width for 11 point Calibri + private const float FontPixelWidth = 7; + + private XmlWriter writer; + private ReferenceManager referenceManager; + private bool hasOpenRowTag; + + private readonly int columnCount; + private bool autoFilterColumns; + private bool hasStartedSheetData; + + /// + /// Initializes a new instance of the ExcelSheet class. + /// + /// XmlWriter to write the sheet data + /// Number of columns in the new sheet + internal ExcelSheet(XmlWriter writer, int columnCount) + { + this.writer = writer; + this.columnCount = columnCount; + + writer.WriteStartDocument(); + writer.WriteStartElement("worksheet", "http://schemas.openxmlformats.org/spreadsheetml/2006/main"); + writer.WriteAttributeString("xmlns", "r", null, "http://schemas.openxmlformats.org/officeDocument/2006/relationships"); + + referenceManager = new ReferenceManager(writer); + } + + /// + /// Start a new row + /// + public void AddRow() + { + // Write the open tag for sheetData if it hasn't been written yet + if (!hasStartedSheetData) + { + writer.WriteStartElement("sheetData"); + hasStartedSheetData = true; + } + + EndRowIfNeeded(); + hasOpenRowTag = true; + + referenceManager.AssureRowReference(); + + writer.WriteStartElement("row"); + referenceManager.WriteAndIncreaseRowReference(); + } + + /// + /// Write a string cell + /// + /// String value to write + /// Whether the cell should be bold, defaults to false + public void AddCell(string value, bool bold = false) + { + // string needs string + // This class uses inlineStr instead of more common shared string table + // to improve write performance and reduce implementation complexity + referenceManager.AssureColumnReference(); + if (value == null) + { + AddCellEmpty(); + return; + } + + writer.WriteStartElement("c"); + + referenceManager.WriteAndIncreaseColumnReference(); + + 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("t"); + writer.WriteValue(value); + writer.WriteEndElement(); // + writer.WriteEndElement(); // + + writer.WriteEndElement(); // + } + + /// + /// Write a object cell + /// + /// The program will try to output number/datetime, otherwise, call the ToString + /// DbCellValue to write based on data type + /// Whether the cell should be bold, defaults to false + public void AddCell(DbCellValue dbCellValue, bool bold = false) + { + object o = dbCellValue.RawObject; + if (dbCellValue.IsNull || o == null) + { + AddCellEmpty(); + return; + } + switch (Type.GetTypeCode(o.GetType())) + { + case TypeCode.Boolean: + AddCell((bool)o); + break; + case TypeCode.Byte: + case TypeCode.Int16: + case TypeCode.Int32: + case TypeCode.Int64: + case TypeCode.Single: + case TypeCode.Double: + case TypeCode.Decimal: + AddCellBoxedNumber(o); + break; + case TypeCode.DateTime: + AddCell((DateTime)o); + break; + case TypeCode.String: + AddCell((string)o, bold); + break; + default: + if (o is TimeSpan span) //TimeSpan doesn't have TypeCode + { + AddCell(span); + } + // 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) + { + AddCellBoxedNumber(dbCellValue.DisplayValue); + } + else + { + AddCell(dbCellValue.DisplayValue, bold); + } + break; + } + } + + /// + /// Write a sheetView that freezes the top row. Must be called before any rows have been added. + /// + /// Thrown if called after any rows have been added. + 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(); // + + writer.WriteStartElement("selection"); + writer.WriteAttributeString("pane", "bottomLeft"); + writer.WriteEndElement(); // + + writer.WriteEndElement(); // + writer.WriteEndElement(); // + } + + /// + /// Enable auto filtering, the XML will be written when closing the sheet later + /// + public void EnableAutoFilter() + { + autoFilterColumns = true; + } + + /// + /// Write the columns widths. Must be called before any rows have been added. + /// + /// Array with the widths of each column. + /// Thrown if called after any rows have been added. + 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(); // + } + + writer.WriteEndElement(); // + } + + /// + /// Close the tags and close the stream + /// + public void Dispose() + { + EndRowIfNeeded(); + writer.WriteEndElement(); // + + // Write the auto filter XML if requested + if (autoFilterColumns) + { + writer.WriteStartElement("autoFilter"); + writer.WriteAttributeString("ref", $"A1:{ReferenceManager.GetColumnName(this.columnCount)}1"); + writer.WriteEndElement(); // + } + + writer.WriteEndElement(); // + writer.Dispose(); + } + + /// + /// Write a empty cell + /// + /// This only increases the internal bookmark and doesn't actually write out anything. + private void AddCellEmpty() + { + referenceManager.IncreaseColumnReference(); + } + + /// + /// Write a bool cell. + /// + /// Boolean value to write + private void AddCell(bool value) + { + // Excel FALSE: 0 + // Excel TRUE: 1 + referenceManager.AssureColumnReference(); + + writer.WriteStartElement("c"); + + referenceManager.WriteAndIncreaseColumnReference(); + + writer.WriteAttributeString("t", "b"); + + writer.WriteStartElement("v"); + if (value) + { + writer.WriteValue("1"); //use string to avoid convert + } + else + { + writer.WriteValue("0"); + } + writer.WriteEndElement(); // + + writer.WriteEndElement(); // + } + + /// + /// Write a TimeSpan cell. + /// + /// TimeSpan value to write + private void AddCell(TimeSpan time) + { + referenceManager.AssureColumnReference(); + double excelDate = (double)time.Ticks / (double)TicksPerDay; + // The default hh:mm:ss format do not support more than 24 hours + // For that case, use the format string [h]:mm:ss + if (time.Ticks >= TicksPerDay) + { + AddCellDateTimeInternal(excelDate, Style.TimeMoreThan24Hours); + } + else + { + AddCellDateTimeInternal(excelDate, Style.Time); + } + } + + /// + /// Write a DateTime cell. + /// + /// DateTime value to write + /// + /// 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. + /// Otherwise, save as datetime, and if the time is 00:00:00, show as yyyy-MM-dd. + /// Show the datetime as yyyy-MM-dd HH:mm:ss if none of the previous situations + /// + private void AddCell(DateTime dateTime) + { + referenceManager.AssureColumnReference(); + long ticks = dateTime.Ticks; + Style style = Style.DateTime; + double excelDate; + if (ticks < TicksPerDay) //date empty, time only + { + style = Style.Time; + excelDate = ((double)ticks) / (double)TicksPerDay; + } + else if (ticks < ExcelDateCutoffTick) //before excel cut-off, use string + { + AddCell(dateTime.ToString("yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture)); + return; + } + else + { + if (ticks % TicksPerDay == 0) //time empty, date only + { + style = Style.Date; + } + excelDate = ((double)(ticks - ExcelEpochTick)) / (double)TicksPerDay; + } + AddCellDateTimeInternal(excelDate, style); + } + + // number needs 12.5 + private void AddCellBoxedNumber(object number) + { + referenceManager.AssureColumnReference(); + + writer.WriteStartElement("c"); + + referenceManager.WriteAndIncreaseColumnReference(); + + writer.WriteStartElement("v"); + writer.WriteValue(number); + writer.WriteEndElement(); // + + writer.WriteEndElement(); // + } + + + // datetime needs 26012.451 + private void AddCellDateTimeInternal(double excelDate, Style style) + { + writer.WriteStartElement("c"); + + referenceManager.WriteAndIncreaseColumnReference(); + + writer.WriteStartAttribute("s"); + writer.WriteValue((int)style); + writer.WriteEndAttribute(); + + writer.WriteStartElement("v"); + writer.WriteValue(excelDate); + writer.WriteEndElement(); // + + writer.WriteEndElement(); // + } + + private void EndRowIfNeeded() + { + if (hasOpenRowTag) + { + writer.WriteEndElement(); // + } + } + } + + /// + /// Helper class to track the current cell reference. + /// + /// + /// SpreadsheetML cell needs a reference attribute. (e.g. r="A1"). This class is used + /// to track the current cell reference. + /// + internal class ReferenceManager + { + private int currColumn; // 0 is invalid, the first AddRow will set to 1 + private int currRow = 1; + + // In order to reduce allocation, current reference is saved in this array, + // and write to the XmlWriter through WriteChars. + // For example, when the reference has value AA15, + // The content of this array will be @AA15xxxxx, with currReferenceRowLength=2 + // and currReferenceColumnLength=2 + private char[] currReference = new char[3 + 7]; //maximal XFD1048576 + private int currReferenceRowLength; + private int currReferenceColumnLength; + + private XmlWriter writer; + + /// + /// Initializes a new instance of the ReferenceManager class. + /// + /// XmlWriter to write the reference attribute to. + public ReferenceManager(XmlWriter writer) + { + this.writer = writer; + } + + /// + /// Check that we have not write too many columns. (xlsx has a limit of 16384 columns) + /// + public void AssureColumnReference() + { + if (currColumn == 0) + { + throw new InvalidOperationException("AddRow must be called before AddCell"); + + } + if (currColumn > 16384) + { + throw new InvalidOperationException("max column number is 16384, see https://support.office.com/en-us/article/Excel-specifications-and-limits-1672b34d-7043-467e-8e27-269d656771c3"); + } + } + + /// + /// Write out the r="A1" attribute and increase the column number of internal bookmark + /// + public void WriteAndIncreaseColumnReference() + { + writer.WriteStartAttribute("r"); + writer.WriteChars(currReference, 3 - currReferenceColumnLength, currReferenceRowLength + currReferenceColumnLength); + writer.WriteEndAttribute(); + IncreaseColumnReference(); + } + + /// + /// Increase the column of internal bookmark. + /// + public void IncreaseColumnReference() + { + // This function change the first three chars of currReference array + // The logic is simple, when a start a new row, the array is reset to @@A + // where @='A'-1. At each increase, check if the current reference is Z + // and move to AA if needed, since the maximal is 16384, or XFD, the code + // manipulates the array element directly instead of loop + char[] reference = currReference; + currColumn++; + if ('Z' == reference[2]++) + { + reference[2] = 'A'; + if (currReferenceColumnLength < 2) + { + currReferenceColumnLength = 2; + } + if ('Z' == reference[1]++) + { + reference[0]++; + reference[1] = 'A'; + currReferenceColumnLength = 3; + } + } + } + + /// + /// 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 + /// + /// The column number. + /// The column name. + 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; + } + + /// + /// Check that we have not write too many rows. (xlsx has a limit of 1048576 rows) + /// + public void AssureRowReference() + { + if (currRow > 1048576) + { + throw new InvalidOperationException("max row number is 1048576, see https://support.office.com/en-us/article/Excel-specifications-and-limits-1672b34d-7043-467e-8e27-269d656771c3"); + } + } + /// + /// Write out the r="1" attribute and increase the row number of internal bookmark + /// + public void WriteAndIncreaseRowReference() + { + writer.WriteStartAttribute("r"); + writer.WriteValue(currRow); + writer.WriteEndAttribute(); + + ResetColumnReference(); //This need to be called before the increase + + currRow++; + } + + // Reset the Column Reference + // This will reset the first three chars of currReference array to '@@A' + // and the rest to the array to the string presentation of the current row. + private void ResetColumnReference() + { + currColumn = 1; + currReference[0] = currReference[1] = (char)('A' - 1); + currReference[2] = 'A'; + currReferenceColumnLength = 1; + + string rowReference = XmlConvert.ToString(currRow); + currReferenceRowLength = rowReference.Length; + rowReference.CopyTo(0, currReference, 3, rowReference.Length); + } + } + + private enum Style + { + Normal = 0, + Date = 1, + Time = 2, + DateTime = 3, + TimeMoreThan24Hours = 4, + } + + private ZipArchive zipArchive; + private List sheetNames = new List(); + private XmlWriterSettings writerSetting = new XmlWriterSettings() + { + CloseOutput = true, + }; + + /// + /// Initializes a new instance of the SaveAsExcelFileStreamWriterHelper class. + /// + /// The input or output stream. + public SaveAsExcelFileStreamWriterHelper(Stream stream) + { + zipArchive = new ZipArchive(stream, ZipArchiveMode.Create, false); + } + + /// + /// Initializes a new instance of the SaveAsExcelFileStreamWriterHelper class. + /// + /// The input or output stream. + /// true to leave the stream open after the + /// SaveAsExcelFileStreamWriterHelper object is disposed; otherwise, false. + public SaveAsExcelFileStreamWriterHelper(Stream stream, bool leaveOpen) + { + zipArchive = new ZipArchive(stream, ZipArchiveMode.Create, leaveOpen); + } + + /// + /// Add sheet inside the Xlsx file. + /// + /// Sheet name + /// ExcelSheet for writing the sheet content + /// + /// When the sheetName is null, sheet1,sheet2,..., will be used. + /// The following characters are not allowed in the sheetName + /// '\', '/','*','[',']',':','?' + /// + public ExcelSheet AddSheet(string sheetName, int columnCount) + { + string sheetFileName = "sheet" + (sheetNames.Count + 1); + sheetName = sheetName ?? sheetFileName; + EnsureValidSheetName(sheetName); + + sheetNames.Add(sheetName); + XmlWriter sheetWriter = AddEntry($"xl/worksheets/{sheetFileName}.xml"); + return new ExcelSheet(sheetWriter, columnCount); + } + + /// + /// Write out the rest of the xlsx files and release the resources used by the current instance + /// + public void Dispose() + { + WriteMinimalTemplate(); + zipArchive.Dispose(); + } + + + private XmlWriter AddEntry(string entryName) + { + ZipArchiveEntry entry = zipArchive.CreateEntry(entryName, CompressionLevel.Fastest); + return XmlWriter.Create(entry.Open(), writerSetting); + } + + //ECMA-376 page 75 + private void WriteMinimalTemplate() + { + WriteTopRel(); + WriteWorkbook(); + WriteStyle(); + WriteContentType(); + WriteWorkbookRel(); + } + + /// + /// write [Content_Types].xml + /// + /// + /// This file need to describe all the files in the zip. + /// + private void WriteContentType() + { + using (XmlWriter xw = AddEntry("[Content_Types].xml")) + { + xw.WriteStartDocument(); + xw.WriteStartElement("Types", "http://schemas.openxmlformats.org/package/2006/content-types"); + + xw.WriteStartElement("Default"); + xw.WriteAttributeString("Extension", "rels"); + xw.WriteAttributeString("ContentType", "application/vnd.openxmlformats-package.relationships+xml"); + xw.WriteEndElement(); // + + xw.WriteStartElement("Override"); + xw.WriteAttributeString("PartName", "/xl/workbook.xml"); + xw.WriteAttributeString("ContentType", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"); + xw.WriteEndElement(); // + + xw.WriteStartElement("Override"); + xw.WriteAttributeString("PartName", "/xl/styles.xml"); + xw.WriteAttributeString("ContentType", "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"); + xw.WriteEndElement(); // + + for (int i = 1; i <= sheetNames.Count; ++i) + { + xw.WriteStartElement("Override"); + xw.WriteAttributeString("PartName", "/xl/worksheets/sheet" + i + ".xml"); + xw.WriteAttributeString("ContentType", "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"); + xw.WriteEndElement(); // + } + xw.WriteEndElement(); // + xw.WriteEndDocument(); + } + } + + /// + /// Write _rels/.rels. This file only need to reference main workbook + /// + private void WriteTopRel() + { + using (XmlWriter xw = AddEntry("_rels/.rels")) + { + xw.WriteStartDocument(); + + xw.WriteStartElement("Relationships", "http://schemas.openxmlformats.org/package/2006/relationships"); + + xw.WriteStartElement("Relationship"); + xw.WriteAttributeString("Id", "rId1"); + xw.WriteAttributeString("Type", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument"); + xw.WriteAttributeString("Target", "xl/workbook.xml"); + xw.WriteEndElement(); // + + xw.WriteEndElement(); // + + xw.WriteEndDocument(); + } + } + + private static char[] invalidSheetNameCharacters = new char[] + { + '\\', '/','*','[',']',':','?' + }; + private void EnsureValidSheetName(string sheetName) + { + if (sheetName.IndexOfAny(invalidSheetNameCharacters) != -1) + { + throw new ArgumentException($"Invalid sheetname: sheetName"); + } + if (sheetNames.IndexOf(sheetName) != -1) + { + throw new ArgumentException($"Duplicate sheetName: {sheetName}"); + } + } + + /// + /// Write xl/workbook.xml. This file will references the sheets through ids in xl/_rels/workbook.xml.rels + /// + private void WriteWorkbook() + { + using (XmlWriter xw = AddEntry("xl/workbook.xml")) + { + xw.WriteStartDocument(); + xw.WriteStartElement("workbook", "http://schemas.openxmlformats.org/spreadsheetml/2006/main"); + xw.WriteAttributeString("xmlns", "r", null, "http://schemas.openxmlformats.org/officeDocument/2006/relationships"); + xw.WriteStartElement("sheets"); + for (int i = 1; i <= sheetNames.Count; i++) + { + xw.WriteStartElement("sheet"); + xw.WriteAttributeString("name", sheetNames[i - 1]); + xw.WriteAttributeString("sheetId", i.ToString()); + xw.WriteAttributeString("r", "id", null, "rId" + i); + xw.WriteEndElement(); // + } + xw.WriteEndDocument(); + } + } + + /// + /// Write xl/_rels/workbook.xml.rels. This file will have the paths of the style and sheets. + /// + private void WriteWorkbookRel() + { + using (XmlWriter xw = AddEntry("xl/_rels/workbook.xml.rels")) + { + xw.WriteStartDocument(); + xw.WriteStartElement("Relationships", "http://schemas.openxmlformats.org/package/2006/relationships"); + + xw.WriteStartElement("Relationship"); + xw.WriteAttributeString("Id", "rId0"); + xw.WriteAttributeString("Type", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles"); + xw.WriteAttributeString("Target", "styles.xml"); + xw.WriteEndElement(); // + + for (int i = 1; i <= sheetNames.Count; i++) + { + xw.WriteStartElement("Relationship"); + xw.WriteAttributeString("Id", "rId" + i); + xw.WriteAttributeString("Type", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"); + xw.WriteAttributeString("Target", "worksheets/sheet" + i + ".xml"); + xw.WriteEndElement(); // + } + xw.WriteEndElement(); // + xw.WriteEndDocument(); + } + } + + // Write the xl/styles.xml + private void WriteStyle() + { + // the style 0 is used for general case, style 1 for date, style 2 for time and style 3 for datetime see Enum Style + // reference chain: (index start with 0) + // (in sheet1.xml) --> (by s) --> (by xfId) + // --> (by numFmtId) + // that is will reference the second element of + // then, this xf reference numFmt by name and get formatCode "hh:mm:ss" + + using (XmlWriter xw = AddEntry("xl/styles.xml")) + { + xw.WriteStartElement("styleSheet", "http://schemas.openxmlformats.org/spreadsheetml/2006/main"); + + xw.WriteStartElement("numFmts"); + xw.WriteAttributeString("count", "4"); + xw.WriteStartElement("numFmt"); + xw.WriteAttributeString("numFmtId", "166"); + xw.WriteAttributeString("formatCode", "yyyy-mm-dd"); + xw.WriteEndElement(); // + xw.WriteStartElement("numFmt"); + xw.WriteAttributeString("numFmtId", "167"); + xw.WriteAttributeString("formatCode", "hh:mm:ss"); + xw.WriteEndElement(); // + xw.WriteStartElement("numFmt"); + xw.WriteAttributeString("numFmtId", "168"); + xw.WriteAttributeString("formatCode", "yyyy-mm-dd hh:mm:ss"); + xw.WriteEndElement(); // + xw.WriteStartElement("numFmt"); + xw.WriteAttributeString("numFmtId", "169"); + xw.WriteAttributeString("formatCode", "[h]:mm:ss"); + xw.WriteEndElement(); // + xw.WriteEndElement(); // + + + xw.WriteStartElement("fonts"); + xw.WriteAttributeString("count", "2"); + + xw.WriteStartElement("font"); + xw.WriteStartElement("sz"); + xw.WriteAttributeString("val", "11"); + xw.WriteEndElement(); // + xw.WriteStartElement("color"); + xw.WriteAttributeString("theme", "1"); + xw.WriteEndElement(); // + xw.WriteStartElement("name"); + xw.WriteAttributeString("val", "Calibri"); + xw.WriteEndElement(); // + xw.WriteStartElement("family"); + xw.WriteAttributeString("val", "2"); + xw.WriteEndElement(); // + xw.WriteStartElement("scheme"); + xw.WriteAttributeString("val", "minor"); + xw.WriteEndElement(); // + xw.WriteEndElement(); // + + xw.WriteStartElement("font"); + xw.WriteStartElement("b"); + xw.WriteEndElement(); // + xw.WriteStartElement("sz"); + xw.WriteAttributeString("val", "11"); + xw.WriteEndElement(); // + xw.WriteStartElement("color"); + xw.WriteAttributeString("theme", "1"); + xw.WriteEndElement(); // + xw.WriteStartElement("name"); + xw.WriteAttributeString("val", "Calibri"); + xw.WriteEndElement(); // + xw.WriteStartElement("family"); + xw.WriteAttributeString("val", "2"); + xw.WriteEndElement(); // + xw.WriteStartElement("scheme"); + xw.WriteAttributeString("val", "minor"); + xw.WriteEndElement(); // + xw.WriteEndElement(); // + + xw.WriteEndElement(); // fonts + + xw.WriteStartElement("fills"); + xw.WriteAttributeString("count", "1"); + xw.WriteStartElement("fill"); + xw.WriteStartElement("patternFill"); + xw.WriteAttributeString("patternType", "none"); + xw.WriteEndElement(); // + xw.WriteEndElement(); // + xw.WriteEndElement(); // + + xw.WriteStartElement("borders"); + xw.WriteAttributeString("count", "1"); + xw.WriteStartElement("border"); + xw.WriteElementString("left", null); + xw.WriteElementString("right", null); + xw.WriteElementString("top", null); + xw.WriteElementString("bottom", null); + xw.WriteElementString("diagonal", null); + xw.WriteEndElement(); // + xw.WriteEndElement(); // + + xw.WriteStartElement("cellStyleXfs"); + xw.WriteAttributeString("count", "1"); + xw.WriteStartElement("xf"); + xw.WriteAttributeString("numFmtId", "0"); + xw.WriteAttributeString("fontId", "0"); + xw.WriteAttributeString("fillId", "0"); + xw.WriteAttributeString("borderId", "0"); + xw.WriteEndElement(); // + xw.WriteEndElement(); // + + xw.WriteStartElement("cellXfs"); + xw.WriteAttributeString("count", "6"); + xw.WriteStartElement("xf"); + xw.WriteAttributeString("xfId", "0"); + xw.WriteEndElement(); // + xw.WriteStartElement("xf"); + xw.WriteAttributeString("numFmtId", "166"); + xw.WriteAttributeString("xfId", "0"); + xw.WriteAttributeString("applyNumberFormat", "1"); + xw.WriteEndElement(); // + xw.WriteStartElement("xf"); + xw.WriteAttributeString("numFmtId", "167"); + xw.WriteAttributeString("xfId", "0"); + xw.WriteAttributeString("applyNumberFormat", "1"); + xw.WriteEndElement(); // + xw.WriteStartElement("xf"); + xw.WriteAttributeString("numFmtId", "168"); + xw.WriteAttributeString("xfId", "0"); + xw.WriteAttributeString("applyNumberFormat", "1"); + xw.WriteEndElement(); // + xw.WriteStartElement("xf"); + xw.WriteAttributeString("numFmtId", "169"); + xw.WriteAttributeString("xfId", "0"); + xw.WriteAttributeString("applyNumberFormat", "1"); + xw.WriteEndElement(); // + xw.WriteStartElement("xf"); + xw.WriteAttributeString("xfId", "0"); + xw.WriteAttributeString("fontId", "1"); + xw.WriteEndElement(); // + xw.WriteEndElement(); // + + xw.WriteStartElement("cellStyles"); + xw.WriteAttributeString("count", "1"); + xw.WriteStartElement("cellStyle"); + xw.WriteAttributeString("name", "Normal"); + xw.WriteAttributeString("builtinId", "0"); + xw.WriteAttributeString("xfId", "0"); + xw.WriteEndElement(); // + xw.WriteEndElement(); // + } + } + } +} diff --git a/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsJsonFileStreamFactory.cs b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsJsonFileStreamFactory.cs new file mode 100644 index 00000000..4abf5ea3 --- /dev/null +++ b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsJsonFileStreamFactory.cs @@ -0,0 +1,74 @@ +// +// 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.IO; +using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using Microsoft.SqlTools.ServiceLayer.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage +{ + public class SaveAsJsonFileStreamFactory : IFileStreamFactory + { + + #region Properties + + /// + /// Parameters for the save as JSON request + /// + public SaveResultsAsJsonRequestParams SaveRequestParams { get; set; } + + #endregion + + /// + /// File names are not meant to be created with this factory. + /// + /// Thrown all times + [Obsolete] + public string CreateFile() + { + throw new InvalidOperationException(); + } + + /// + /// Returns a new service buffer reader for reading results back in from the temporary buffer files, file share is ReadWrite to allow concurrent reads/writes to the file. + /// + /// Path to the temp buffer file + /// Stream reader + public IFileStreamReader GetReader(string fileName) + { + return new ServiceBufferFileStreamReader(new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)); + } + + /// + /// Returns a new JSON writer for writing results to a JSON file, file share is ReadWrite to allow concurrent reads/writes to the file. + /// + /// Path to the JSON output file + /// + /// The entire list of columns for the result set. They will be filtered down as per the + /// request params. + /// + /// Stream writer + public IFileStreamWriter GetWriter(string fileName, IReadOnlyList columns) + { + return new SaveAsJsonFileStreamWriter( + new FileStream(fileName, FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite), + SaveRequestParams, + columns + ); + } + + /// + /// Safely deletes the file + /// + /// Path to the file to delete + public void DisposeFile(string fileName) + { + FileUtilities.SafeFileDelete(fileName); + } + + } +} diff --git a/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsJsonFileStreamWriter.cs b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsJsonFileStreamWriter.cs new file mode 100644 index 00000000..9eb302e8 --- /dev/null +++ b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsJsonFileStreamWriter.cs @@ -0,0 +1,113 @@ +// +// 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.IO; +using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using Newtonsoft.Json; + +namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage +{ + /// + /// Writer for writing rows of results to a JSON file. + /// + /// + /// This implements its own IDisposable because the cleanup logic closes the array that was + /// created when the writer was created. Since this behavior is different than the standard + /// file stream cleanup, the extra Dispose method was added. + /// + public class SaveAsJsonFileStreamWriter : SaveAsStreamWriter, IDisposable + { + #region Member Variables + + private readonly StreamWriter streamWriter; + private readonly JsonWriter jsonWriter; + + #endregion + + /// + /// Constructor, writes the header to the file, chains into the base constructor + /// + /// FileStream to access the JSON file output + /// JSON save as request parameters + /// + /// The entire list of columns for the result set. They will be filtered down as per the + /// request params. + /// + public SaveAsJsonFileStreamWriter(Stream stream, SaveResultsRequestParams requestParams, IReadOnlyList columns) + : base(stream, requestParams, columns) + { + // Setup the internal state + streamWriter = new StreamWriter(stream); + jsonWriter = new JsonTextWriter(streamWriter); + jsonWriter.Formatting = Formatting.Indented; + + // Write the header of the file + jsonWriter.WriteStartArray(); + } + + /// + /// Writes a row of data as a JSON object + /// + /// The data of the row to output to the file + /// + /// The entire list of columns for the result set. They will be filtered down as per the + /// request params. + /// + public override void WriteRow(IList row, IReadOnlyList columns) + { + // Write the header for the object + jsonWriter.WriteStartObject(); + + // Write the items out as properties + for (int i = ColumnStartIndex; i <= ColumnEndIndex; i++) + { + jsonWriter.WritePropertyName(columns[i].ColumnName); + if (row[i].RawObject == null) + { + jsonWriter.WriteNull(); + } + else + { + // Try converting to column type + try + { + var value = Convert.ChangeType(row[i].DisplayValue, columns[i].DataType); + jsonWriter.WriteValue(value); + } + // Default column type as string + catch + { + jsonWriter.WriteValue(row[i].DisplayValue); + } + } + } + + // Write the footer for the object + jsonWriter.WriteEndObject(); + } + + private bool disposed = false; + /// + /// Disposes the writer by closing up the array that contains the row objects + /// + protected override void Dispose(bool disposing) + { + if (disposed) + return; + + if (disposing) + { + // Write the footer of the file + jsonWriter.WriteEndArray(); + // This closes the underlying stream, so we needn't call close on the underlying stream explicitly + jsonWriter.Close(); + } + disposed = true; + base.Dispose(disposing); + } + } +} diff --git a/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsMarkdownFileStreamFactory.cs b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsMarkdownFileStreamFactory.cs new file mode 100644 index 00000000..ed159706 --- /dev/null +++ b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsMarkdownFileStreamFactory.cs @@ -0,0 +1,63 @@ +// +// 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.IO; +using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using Microsoft.SqlTools.ServiceLayer.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage +{ + public class SaveAsMarkdownFileStreamFactory : IFileStreamFactory + { + private readonly SaveResultsAsMarkdownRequestParams _saveRequestParams; + + /// + /// Constructs and initializes a new instance of . + /// + /// Parameters for the save as request + public SaveAsMarkdownFileStreamFactory(SaveResultsAsMarkdownRequestParams requestParams) + { + this._saveRequestParams = requestParams; + } + + /// + /// Throw at all times. + [Obsolete("Not implemented for export factories.")] + public string CreateFile() + { + throw new InvalidOperationException("CreateFile not implemented for export factories"); + } + + /// + /// + /// Returns an instance of the . + /// + public IFileStreamReader GetReader(string fileName) + { + return new ServiceBufferFileStreamReader( + new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)); + } + + /// + /// + /// Returns an instance of the . + /// + public IFileStreamWriter GetWriter(string fileName, IReadOnlyList columns) + { + return new SaveAsMarkdownFileStreamWriter( + new FileStream(fileName, FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite), + this._saveRequestParams, + columns); + } + + /// + public void DisposeFile(string fileName) + { + FileUtilities.SafeFileDelete(fileName); + } + } +} \ No newline at end of file diff --git a/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsMarkdownFileStreamWriter.cs b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsMarkdownFileStreamWriter.cs new file mode 100644 index 00000000..337a8c2b --- /dev/null +++ b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsMarkdownFileStreamWriter.cs @@ -0,0 +1,103 @@ +// +// 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.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Web; +using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage +{ + /// + /// Writer for exporting results to a Markdown table. + /// + public partial class SaveAsMarkdownFileStreamWriter : SaveAsStreamWriter + { + private const string Delimiter = "|"; + private static Regex _newLineRegex = new Regex("(\\r\\n|\\n|\\r)", RegexOptions.Compiled); + + private static Regex GetNewLineRegex() => _newLineRegex; + + private readonly Encoding _encoding; + private readonly string _lineSeparator; + + public SaveAsMarkdownFileStreamWriter( + Stream stream, + SaveResultsAsMarkdownRequestParams requestParams, + IReadOnlyList columns) + : base(stream, requestParams, columns) + { + // Parse the request params + this._lineSeparator = string.IsNullOrEmpty(requestParams.LineSeparator) + ? Environment.NewLine + : requestParams.LineSeparator; + this._encoding = ParseEncoding(requestParams.Encoding, Encoding.UTF8); + + // Output the header if requested + if (requestParams.IncludeHeaders) + { + // Write the column header + IEnumerable selectedColumnNames = columns.Skip(this.ColumnStartIndex) + .Take(this.ColumnCount) + .Select(c => EncodeMarkdownField(c.ColumnName)); + string headerLine = string.Join(Delimiter, selectedColumnNames); + + this.WriteLine($"{Delimiter}{headerLine}{Delimiter}"); + + // Write the separator row + var separatorBuilder = new StringBuilder(Delimiter); + for (int i = 0; i < this.ColumnCount; i++) + { + separatorBuilder.Append($"---{Delimiter}"); + } + + this.WriteLine(separatorBuilder.ToString()); + } + } + + /// + public override void WriteRow(IList row, IReadOnlyList columns) + { + IEnumerable selectedCells = row.Skip(this.ColumnStartIndex) + .Take(this.ColumnCount) + .Select(c => EncodeMarkdownField(c.DisplayValue)); + string rowLine = string.Join(Delimiter, selectedCells); + + this.WriteLine($"{Delimiter}{rowLine}{Delimiter}"); + } + + internal static string EncodeMarkdownField(string field) + { + // Special case for nulls + if (field == null) + { + return "NULL"; + } + + // Escape HTML entities, since Markdown supports inline HTML + field = HttpUtility.HtmlEncode(field); + + // Escape pipe delimiters + field = field.Replace(@"|", @"\|"); + + // @TODO: Allow option to encode multiple whitespace characters as   + + // Replace newlines with br tags, since cell values must be single line + field = GetNewLineRegex().Replace(field, @"
"); + + return field; + } + + private void WriteLine(string line) + { + byte[] bytes = this._encoding.GetBytes(line + this._lineSeparator); + this.FileStream.Write(bytes, 0, bytes.Length); + } + } +} \ No newline at end of file diff --git a/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsWriterBase.cs b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsWriterBase.cs new file mode 100644 index 00000000..e3248ec8 --- /dev/null +++ b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsWriterBase.cs @@ -0,0 +1,180 @@ +// +// 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.IO; +using System.Text; +using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using Microsoft.SqlTools.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage +{ + /// + /// Abstract class for implementing writers that save results to file. Stores some basic info + /// that all save as writer would need. + /// + public abstract class SaveAsStreamWriter : IFileStreamWriter + { + /// + /// Stores the internal state for the writer that will be necessary for any writer. + /// + /// The stream that will be written to + /// The SaveAs request parameters + /// + /// The entire list of columns for the result set. Used to determine which columns to + /// output. + /// + protected SaveAsStreamWriter(Stream stream, SaveResultsRequestParams requestParams, IReadOnlyList columns) + { + Validate.IsNotNull(nameof(stream), stream); + Validate.IsNotNull(nameof(columns), columns); + + FileStream = stream; + if (requestParams.IsSaveSelection) + { + // ReSharper disable PossibleInvalidOperationException IsSaveSelection verifies these values exist + ColumnStartIndex = requestParams.ColumnStartIndex.Value; + ColumnEndIndex = requestParams.ColumnEndIndex.Value; + // ReSharper restore PossibleInvalidOperationException + } + else + { + // Save request was for the entire result set, use default start/end + ColumnStartIndex = 0; + ColumnEndIndex = columns.Count - 1; + } + + ColumnCount = ColumnEndIndex - ColumnStartIndex + 1; + } + + #region Properties + + /// + /// Index of the first column to write to the output file + /// + protected int ColumnStartIndex { get; } + + /// + /// Number of columns to write to the output file + /// + protected int ColumnCount { get; } + + /// + /// Index of the last column to write to the output file (inclusive). + /// + protected int ColumnEndIndex { get; } + + /// + /// The file stream to use to write the output file + /// + protected Stream FileStream { get; } + + #endregion + + /// + /// Not implemented, do not use. + /// + [Obsolete] + public int WriteRow(StorageDataReader dataReader) + { + throw new InvalidOperationException("This type of writer is meant to write values from a list of cell values only."); + } + + /// + /// Indicates whether columns should be measured based on whether the output format supports it and if the caller wants the columns automatically sized + /// + public virtual bool ShouldMeasureRowColumns => false; + + /// + /// Measures the columns in a row of data as part of determining automatic column widths + /// + /// The row of data to measure + public virtual void MeasureRowColumns(IList row) { } + + /// + /// Writes a row of data to the output file using the format provided by the implementing class. + /// + /// The row of data to output + /// The list of columns to output + public abstract void WriteRow(IList row, IReadOnlyList columns); + + /// + /// Not implemented, do not use. + /// + [Obsolete] + public void Seek(long offset) + { + throw new InvalidOperationException("SaveAs writers are meant to be written once contiguously."); + } + + /// + /// Flushes the file stream buffer + /// + public void FlushBuffer() + { + FileStream.Flush(); + } + + /// + /// Attempts to parse the provided and return an encoding that + /// matches the encoding name or codepage number. + /// + /// Encoding name or codepage number to parse. + /// + /// Encoding to return if no encoding of provided name/codepage number exists. + /// + /// + /// Desired encoding object or the if the desired + /// encoding could not be found. + /// + protected static Encoding ParseEncoding(string encoding, Encoding fallbackEncoding) + { + // If the encoding is a number, we try to look up a codepage encoding using the + // parsed number as a codepage. If it is not a number, attempt to look up an + // encoding with the provided encoding name. If getting the encoding fails in + // either case, we will return the fallback encoding. + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + try + { + return int.TryParse(encoding, out int codePage) + ? Encoding.GetEncoding(codePage) + : Encoding.GetEncoding(encoding); + } + catch + { + return fallbackEncoding; + } + } + + #region IDisposable Implementation + + private bool disposed; + + /// + /// Disposes the instance by flushing and closing the file stream + /// + /// + protected virtual void Dispose(bool disposing) + { + if (disposed) + return; + + if (disposing) + { + FileStream.Dispose(); + } + disposed = true; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + #endregion + } +} diff --git a/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsXmlFileStreamFactory.cs b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsXmlFileStreamFactory.cs new file mode 100644 index 00000000..3eda9221 --- /dev/null +++ b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsXmlFileStreamFactory.cs @@ -0,0 +1,76 @@ +// +// 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.IO; +using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using Microsoft.SqlTools.ServiceLayer.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage +{ + public class SaveAsXmlFileStreamFactory : IFileStreamFactory + { + + #region Properties + + /// + /// Parameters for the save as XML request + /// + public SaveResultsAsXmlRequestParams SaveRequestParams { get; set; } + + #endregion + + /// + /// File names are not meant to be created with this factory. + /// + /// Thrown all times + [Obsolete] + public string CreateFile() + { + throw new InvalidOperationException(); + } + + /// + /// Returns a new service buffer reader for reading results back in from the temporary buffer files, file share is ReadWrite to allow concurrent reads/writes to the file. + /// + /// Path to the temp buffer file + /// Stream reader + public IFileStreamReader GetReader(string fileName) + { + return new ServiceBufferFileStreamReader( + new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite) + ); + } + + /// + /// Returns a new XML writer for writing results to a XML file, file share is ReadWrite to allow concurrent reads/writes to the file. + /// + /// Path to the XML output file + /// + /// The entire list of columns for the result set. They will be filtered down as per the + /// request params. + /// + /// Stream writer + public IFileStreamWriter GetWriter(string fileName, IReadOnlyList columns) + { + return new SaveAsXmlFileStreamWriter( + new FileStream(fileName, FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite), + SaveRequestParams, + columns + ); + } + + /// + /// Safely deletes the file + /// + /// Path to the file to delete + public void DisposeFile(string fileName) + { + FileUtilities.SafeFileDelete(fileName); + } + + } +} diff --git a/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsXmlFileStreamWriter.cs b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsXmlFileStreamWriter.cs new file mode 100644 index 00000000..0698525e --- /dev/null +++ b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsXmlFileStreamWriter.cs @@ -0,0 +1,144 @@ +// +// 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.IO; +using System.Text; +using System.Xml; +using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage +{ + /// + /// Writer for writing rows of results to a XML file. + /// + /// + /// This implements its own IDisposable because the cleanup logic closes the element that was + /// created when the writer was created. Since this behavior is different than the standard + /// file stream cleanup, the extra Dispose method was added. + /// + public class SaveAsXmlFileStreamWriter : SaveAsStreamWriter, IDisposable + { + // Root element name for the output XML + private const string RootElementTag = "data"; + + // Item element name which will be used for every row + private const string ItemElementTag = "row"; + + #region Member Variables + + private readonly XmlTextWriter xmlTextWriter; + + #endregion + + /// + /// Constructor, writes the header to the file, chains into the base constructor + /// + /// FileStream to access the JSON file output + /// XML save as request parameters + /// + /// The entire list of columns for the result set. They will be filtered down as per the + /// request params. + /// + public SaveAsXmlFileStreamWriter(Stream stream, SaveResultsAsXmlRequestParams requestParams, IReadOnlyList columns) + : base(stream, requestParams, columns) + { + // Setup the internal state + var encoding = GetEncoding(requestParams); + xmlTextWriter = new XmlTextWriter(stream, encoding); + xmlTextWriter.Formatting = requestParams.Formatted ? Formatting.Indented : Formatting.None; + + //Start the document and the root element + xmlTextWriter.WriteStartDocument(); + xmlTextWriter.WriteStartElement(RootElementTag); + } + + /// + /// Writes a row of data as a XML object + /// + /// The data of the row to output to the file + /// + /// The entire list of columns for the result set. They will be filtered down as per the + /// request params. + /// + public override void WriteRow(IList row, IReadOnlyList columns) + { + // Write the header for the object + xmlTextWriter.WriteStartElement(ItemElementTag); + + // Write the items out as properties + for (int i = ColumnStartIndex; i <= ColumnEndIndex; i++) + { + // Write the column name as item tag + xmlTextWriter.WriteStartElement(columns[i].ColumnName); + + if (row[i].RawObject != null) + { + xmlTextWriter.WriteString(row[i].DisplayValue); + } + + // End the item tag + xmlTextWriter.WriteEndElement(); + } + + // Write the footer for the object + xmlTextWriter.WriteEndElement(); + } + + /// + /// Get the encoding for the XML file according to + /// + /// XML save as request parameters + /// + private Encoding GetEncoding(SaveResultsAsXmlRequestParams requestParams) + { + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + Encoding encoding; + try + { + if (int.TryParse(requestParams.Encoding, out var codepage)) + { + encoding = Encoding.GetEncoding(codepage); + } + else + { + encoding = Encoding.GetEncoding(requestParams.Encoding); + } + } + catch + { + // Fallback encoding when specified codepage is invalid + encoding = Encoding.GetEncoding("utf-8"); + } + + return encoding; + } + + private bool disposed = false; + + /// + /// Disposes the writer by closing up the element that contains the row objects + /// + protected override void Dispose(bool disposing) + { + if (disposed) + return; + + if (disposing) + { + // Write the footer of the file + xmlTextWriter.WriteEndElement(); + xmlTextWriter.WriteEndDocument(); + + xmlTextWriter.Close(); + xmlTextWriter.Dispose(); + } + + disposed = true; + base.Dispose(disposing); + } + } +} \ No newline at end of file diff --git a/MarkMpn.Sql4Cds.Export/DataStorage/ServiceBufferFileStreamFactory.cs b/MarkMpn.Sql4Cds.Export/DataStorage/ServiceBufferFileStreamFactory.cs new file mode 100644 index 00000000..d58ad9de --- /dev/null +++ b/MarkMpn.Sql4Cds.Export/DataStorage/ServiceBufferFileStreamFactory.cs @@ -0,0 +1,66 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using System.IO; +using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using Microsoft.SqlTools.ServiceLayer.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage +{ + /// + /// Factory that creates file reader/writers that process rows in an internal, non-human readable file format + /// + public class ServiceBufferFileStreamFactory : IFileStreamFactory + { + /// + /// Creates a new temporary file + /// + /// The name of the temporary file + public string CreateFile() + { + return Path.GetTempFileName(); + } + + /// + /// Creates a new for reading values back from + /// an SSMS formatted buffer file, file share is ReadWrite to allow concurrent reads/writes to the file. + /// + /// The file to read values from + /// A + public IFileStreamReader GetReader(string fileName) + { + return new ServiceBufferFileStreamReader( + new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite) + ); + } + + /// + /// Creates a new for writing values out to an + /// SSMS formatted buffer file, file share is ReadWrite to allow concurrent reads/writes to the file. + /// + /// The file to write values to + /// + /// Ignored in order to fulfil the contract. + /// @TODO: Refactor this out so that save-as writers do not use the same contract as service buffer writers. + /// + /// A + public IFileStreamWriter GetWriter(string fileName, IReadOnlyList columns) + { + return new ServiceBufferFileStreamWriter( + new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite) + ); + } + + /// + /// Disposes of a file created via this factory + /// + /// The file to dispose of + public void DisposeFile(string fileName) + { + FileUtilities.SafeFileDelete(fileName); + } + } +} diff --git a/MarkMpn.Sql4Cds.Export/DataStorage/ServiceBufferFileStreamReader.cs b/MarkMpn.Sql4Cds.Export/DataStorage/ServiceBufferFileStreamReader.cs new file mode 100644 index 00000000..709aec37 --- /dev/null +++ b/MarkMpn.Sql4Cds.Export/DataStorage/ServiceBufferFileStreamReader.cs @@ -0,0 +1,645 @@ +// +// 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; +using System.Data.SqlTypes; +using System.IO; +using System.Text; +using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using Microsoft.SqlTools.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage +{ + /// + /// Reader for service buffer formatted file streams + /// + public class ServiceBufferFileStreamReader : IFileStreamReader + { + + #region Constants + + private const int DefaultBufferSize = 8192; + private const string DateFormatString = "yyyy-MM-dd"; + private const string TimeFormatString = "HH:mm:ss"; + + #endregion + + #region Member Variables + + private delegate FileStreamReadResult ReadMethod(long fileOffset, long rowId, DbColumnWrapper column); + + private byte[] buffer; + + private readonly Stream fileStream; + + private readonly Dictionary readMethods; + + private readonly Dictionary sqlDBTypeMap; + + #endregion + + /// + /// Constructs a new ServiceBufferFileStreamReader and initializes its state + /// + /// The filestream to read from + /// The query execution settings + public ServiceBufferFileStreamReader(Stream stream) + { + Validate.IsNotNull(nameof(stream), stream); + + // Open file for reading/writing + if (!stream.CanRead || !stream.CanSeek) + { + throw new InvalidOperationException("Stream must be readable and seekable"); + } + fileStream = stream; + + // Create internal buffer + buffer = new byte[DefaultBufferSize]; + + // Create the methods that will be used to read back + readMethods = new Dictionary + { + {typeof(string), (o, id, col) => ReadString(o, id)}, + {typeof(short), (o, id, col) => ReadInt16(o, id)}, + {typeof(int), (o, id, col) => ReadInt32(o, id)}, + {typeof(long), (o, id, col) => ReadInt64(o, id)}, + {typeof(byte), (o, id, col) => ReadByte(o, id)}, + {typeof(char), (o, id, col) => ReadChar(o, id)}, + {typeof(bool), (o, id, col) => ReadBoolean(o, id)}, + {typeof(double), (o, id, col) => ReadDouble(o, id)}, + {typeof(float), (o, id, col) => ReadSingle(o, id)}, + {typeof(decimal), (o, id, col) => ReadDecimal(o, id)}, + {typeof(DateTime), ReadDateTime}, + {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)}, + + {typeof(SqlString), (o, id, col) => ReadString(o, id)}, + {typeof(SqlInt16), (o, id, col) => ReadInt16(o, id)}, + {typeof(SqlInt32), (o, id, col) => ReadInt32(o, id)}, + {typeof(SqlInt64), (o, id, col) => ReadInt64(o, id)}, + {typeof(SqlByte), (o, id, col) => ReadByte(o, id)}, + {typeof(SqlBoolean), (o, id, col) => ReadBoolean(o, id)}, + {typeof(SqlDouble), (o, id, col) => ReadDouble(o, id)}, + {typeof(SqlSingle), (o, id, col) => ReadSingle(o, id)}, + {typeof(SqlDecimal), (o, id, col) => ReadSqlDecimal(o, id)}, + {typeof(SqlDateTime), ReadDateTime}, + {typeof(SqlBytes), (o, id, col) => ReadBytes(o, id)}, + {typeof(SqlBinary), (o, id, col) => ReadBytes(o, id)}, + {typeof(SqlGuid), (o, id, col) => ReadGuid(o, id)}, + {typeof(SqlMoney), (o, id, col) => ReadMoney(o, id)}, + }; + + sqlDBTypeMap = new Dictionary { + {SqlDbType.BigInt, typeof(SqlInt64)}, + {SqlDbType.Binary, typeof(SqlBinary)}, + {SqlDbType.Bit, typeof(SqlBoolean)}, + {SqlDbType.Char, typeof(SqlString)}, + {SqlDbType.Date, typeof(SqlDateTime)}, + {SqlDbType.DateTime, typeof(SqlDateTime)}, + {SqlDbType.DateTime2, typeof(SqlDateTime)}, + {SqlDbType.DateTimeOffset, typeof(DateTimeOffset)}, + {SqlDbType.Decimal, typeof(SqlDecimal)}, + {SqlDbType.Float, typeof(SqlDouble)}, + {SqlDbType.Image, typeof(SqlBinary)}, + {SqlDbType.Int, typeof(SqlInt32)}, + {SqlDbType.Money, typeof(SqlMoney)}, + {SqlDbType.NChar, typeof(SqlString)}, + {SqlDbType.NText, typeof(SqlString)}, + {SqlDbType.NVarChar, typeof(SqlString)}, + {SqlDbType.Real, typeof(SqlSingle)}, + {SqlDbType.SmallDateTime, typeof(SqlDateTime)}, + {SqlDbType.SmallInt, typeof(SqlInt16)}, + {SqlDbType.SmallMoney, typeof(SqlMoney)}, + {SqlDbType.Text, typeof(SqlString)}, + {SqlDbType.Time, typeof(TimeSpan)}, + {SqlDbType.Timestamp, typeof(SqlBinary)}, + {SqlDbType.TinyInt, typeof(SqlByte)}, + {SqlDbType.UniqueIdentifier, typeof(SqlGuid)}, + {SqlDbType.VarBinary, typeof(SqlBinary)}, + {SqlDbType.VarChar, typeof(SqlString)}, + {SqlDbType.Xml, typeof(SqlString)} + }; + } + + #region IFileStreamStorage Implementation + + /// + /// Reads a row from the file, based on the columns provided + /// + /// Offset into the file where the row starts + /// Internal ID of the row to set for all cells in this row + /// The columns that were encoded + /// The objects from the row, ready for output to the client + public IList ReadRow(long fileOffset, long rowId, IEnumerable columns) + { + // Initialize for the loop + long currentFileOffset = fileOffset; + List results = new List(); + + // Iterate over the columns + foreach (DbColumnWrapper column in columns) + { + // We will pivot based on the type of the column + Type colType; + if (column.IsSqlVariant) + { + // For SQL Variant columns, the type is written first in string format + FileStreamReadResult sqlVariantTypeResult = ReadString(currentFileOffset, rowId); + currentFileOffset += sqlVariantTypeResult.TotalLength; + string sqlVariantType = (string)sqlVariantTypeResult.Value.RawObject; + + // If the typename is null, then the whole value is null + if (sqlVariantTypeResult.Value == null || string.IsNullOrEmpty(sqlVariantType)) + { + results.Add(sqlVariantTypeResult.Value); + continue; + } + + // We need to specify the assembly name for SQL types in order to resolve the type correctly. + if (sqlVariantType.StartsWith("System.Data.SqlTypes.")) + { + sqlVariantType = sqlVariantType + ", System.Data.Common"; + } + colType = Type.GetType(sqlVariantType); + if (colType == null) + { + throw new ArgumentException(SR.QueryServiceUnsupportedSqlVariantType(sqlVariantType, column.ColumnName)); + } + } + else + { + Type type; + if (column.SqlDbType == SqlDbType.Udt) + { + type = column.DataType; + } + else if (!sqlDBTypeMap.TryGetValue(column.SqlDbType, out type)) + { + type = typeof(SqlString); + } + colType = type; + } + + // Use the right read function for the type to read the data from the file + ReadMethod readFunc; + if (!readMethods.TryGetValue(colType, out readFunc)) + { + // Treat everything else as a string + readFunc = readMethods[typeof(string)]; + } + FileStreamReadResult result = readFunc(currentFileOffset, rowId, column); + currentFileOffset += result.TotalLength; + results.Add(result.Value); + } + + return results; + } + + #endregion + + #region Private Helpers + + /// + /// Creates a new buffer that is of the specified length if the buffer is not already + /// at least as long as specified. + /// + /// The minimum buffer size + private void AssureBufferLength(int newBufferLength) + { + if (buffer.Length < newBufferLength) + { + buffer = new byte[newBufferLength]; + } + } + + /// + /// Reads the value of a cell from the file wrapper, checks to see if it null using + /// , and converts it to the proper output type using + /// . + /// + /// Offset into the file to read from + /// Internal ID of the row to set on all cells in this row + /// Function to use to convert the buffer to the target type + /// + /// If provided, this function will be used to determine if the value is null + /// + /// Optional function to use to convert the object to a string. + /// Optional parameter indicates whether the culture invariant display value should be provided. + /// The expected type of the cell. Used to keep the code honest + /// The object, a display value, and the length of the value + its length + private FileStreamReadResult ReadCellHelper(long offset, long rowId, + Func convertFunc, + Func isNullFunc = null, + Func toStringFunc = null, + bool setInvariantCultureDisplayValue = false) + { + LengthResult length = ReadLength(offset); + DbCellValue result = new DbCellValue { RowId = rowId }; + + if (isNullFunc == null ? length.ValueLength == 0 : isNullFunc(length.TotalLength)) + { + result.RawObject = null; + result.DisplayValue = SR.QueryServiceCellNull; + result.IsNull = true; + } + else + { + AssureBufferLength(length.ValueLength); + fileStream.Read(buffer, 0, length.ValueLength); + T resultObject = convertFunc(length.ValueLength); + result.RawObject = resultObject; + result.DisplayValue = toStringFunc == null ? result.RawObject.ToString() : toStringFunc(resultObject); + if (setInvariantCultureDisplayValue) + { + string icDisplayValue = string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0}", result.RawObject); + + // Only set the value when it is different from the DisplayValue to reduce the size of the result + // + if (icDisplayValue != result.DisplayValue) + { + result.InvariantCultureDisplayValue = icDisplayValue; + } + } + result.IsNull = false; + } + + return new FileStreamReadResult(result, length.TotalLength); + } + + /// + /// Reads a short from the file at the offset provided + /// + /// Offset into the file to read the short from + /// Internal ID of the row that will be stored in the cell + /// A short + internal FileStreamReadResult ReadInt16(long fileOffset, long rowId) + { + return ReadCellHelper(fileOffset, rowId, length => BitConverter.ToInt16(buffer, 0)); + } + + /// + /// Reads a int from the file at the offset provided + /// + /// Offset into the file to read the int from + /// Internal ID of the row that will be stored in the cell + /// An int + internal FileStreamReadResult ReadInt32(long fileOffset, long rowId) + { + return ReadCellHelper(fileOffset, rowId, length => BitConverter.ToInt32(buffer, 0)); + } + + /// + /// Reads a long from the file at the offset provided + /// + /// Offset into the file to read the long from + /// Internal ID of the row that will be stored in the cell + /// A long + internal FileStreamReadResult ReadInt64(long fileOffset, long rowId) + { + return ReadCellHelper(fileOffset, rowId, length => BitConverter.ToInt64(buffer, 0)); + } + + /// + /// Reads a byte from the file at the offset provided + /// + /// Offset into the file to read the byte from + /// Internal ID of the row that will be stored in the cell + /// A byte + internal FileStreamReadResult ReadByte(long fileOffset, long rowId) + { + return ReadCellHelper(fileOffset, rowId, length => buffer[0]); + } + + /// + /// Reads a char from the file at the offset provided + /// + /// Offset into the file to read the char from + /// Internal ID of the row that will be stored in the cell + /// A char + internal FileStreamReadResult ReadChar(long fileOffset, long rowId) + { + return ReadCellHelper(fileOffset, rowId, length => BitConverter.ToChar(buffer, 0)); + } + + /// + /// Reads a bool from the file at the offset provided + /// + /// Offset into the file to read the bool from + /// Internal ID of the row that will be stored in the cell + /// A bool + internal FileStreamReadResult ReadBoolean(long fileOffset, long rowId) + { + // Override the stringifier with numeric values if the user prefers that + return ReadCellHelper(fileOffset, rowId, length => buffer[0] == 0x1, + toStringFunc: val => val ? "1" : "0"); + } + + /// + /// Reads a single from the file at the offset provided + /// + /// Offset into the file to read the single from + /// Internal ID of the row that will be stored in the cell + /// A single + internal FileStreamReadResult ReadSingle(long fileOffset, long rowId) + { + return ReadCellHelper(fileOffset, rowId, length => BitConverter.ToSingle(buffer, 0), setInvariantCultureDisplayValue: true); + } + + /// + /// Reads a double from the file at the offset provided + /// + /// Offset into the file to read the double from + /// Internal ID of the row that will be stored in the cell + /// A double + internal FileStreamReadResult ReadDouble(long fileOffset, long rowId) + { + return ReadCellHelper(fileOffset, rowId, length => BitConverter.ToDouble(buffer, 0), setInvariantCultureDisplayValue: true); + } + + /// + /// Reads a SqlDecimal from the file at the offset provided + /// + /// Offset into the file to read the SqlDecimal from + /// A SqlDecimal + internal FileStreamReadResult ReadSqlDecimal(long offset, long rowId) + { + return ReadCellHelper(offset, rowId, length => + { + int[] arrInt32 = new int[(length - 3) / 4]; + Buffer.BlockCopy(buffer, 3, arrInt32, 0, length - 3); + return new SqlDecimal(buffer[0], buffer[1], buffer[2] == 1, arrInt32); + }, setInvariantCultureDisplayValue: true); + } + + /// + /// Reads a decimal from the file at the offset provided + /// + /// Offset into the file to read the decimal from + /// A decimal + internal FileStreamReadResult ReadDecimal(long offset, long rowId) + { + return ReadCellHelper(offset, rowId, length => + { + int[] arrInt32 = new int[length / 4]; + Buffer.BlockCopy(buffer, 0, arrInt32, 0, length); + return new decimal(arrInt32); + }, setInvariantCultureDisplayValue: true); + } + + /// + /// Reads a DateTime from the file at the offset provided + /// + /// Offset into the file to read the DateTime from + /// Internal ID of the row that will be stored in the cell + /// Column metadata, used for determining what precision to output + /// A DateTime + internal FileStreamReadResult ReadDateTime(long offset, long rowId, DbColumnWrapper col) + { + return ReadCellHelper(offset, rowId, length => + { + long ticks = BitConverter.ToInt64(buffer, 0); + return new DateTime(ticks); + + }, null, dt => + { + // Switch based on the type of column + string formatString; + if (col.DataTypeName.Equals("DATE", StringComparison.OrdinalIgnoreCase)) + { + // DATE columns should only show the date + formatString = DateFormatString; + } + else if (col.DataTypeName.StartsWith("DATETIME", StringComparison.OrdinalIgnoreCase)) + { + // DATETIME and DATETIME2 columns should show date, time, and a variable number + // of milliseconds (for DATETIME, it is fixed at 3, but returned as null) + // If for some strange reason a scale > 7 is sent, we will cap it at 7 to avoid + // an exception from invalid date/time formatting + int scale = Math.Min(col.NumericScale ?? 3, 7); + formatString = $"{DateFormatString} {TimeFormatString}"; + if (scale > 0) + { + string millisecondString = new string('f', scale); + formatString += $".{millisecondString}"; + } + } + else + { + // For anything else that returns as a CLR DateTime, just show date and time + formatString = $"{DateFormatString} {TimeFormatString}"; + } + + return dt.ToString(formatString); + + }); + } + + /// + /// Reads a DateTimeOffset from the file at the offset provided + /// + /// 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, DbColumnWrapper col) + { + // DateTimeOffset is represented by DateTime.Ticks followed by TimeSpan.Ticks + // both as Int64 values + return ReadCellHelper(offset, rowId, length => + { + long dtTicks = BitConverter.ToInt64(buffer, 0); + long dtOffset = BitConverter.ToInt64(buffer, 8); + return new DateTimeOffset(new DateTime(dtTicks), new TimeSpan(dtOffset)); + }, null, dt => + { + 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); + }); + } + + /// + /// Reads a TimeSpan from the file at the offset provided + /// + /// Offset into the file to read the TimeSpan from + /// Internal ID of the row that will be stored in the cell + /// A TimeSpan + internal FileStreamReadResult ReadTimeSpan(long offset, long rowId) + { + return ReadCellHelper(offset, rowId, length => + { + long ticks = BitConverter.ToInt64(buffer, 0); + return new TimeSpan(ticks); + }); + } + + /// + /// Reads a string from the file at the offset provided + /// + /// Offset into the file to read the string from + /// Internal ID of the row that will be stored in the cell + /// A string + internal FileStreamReadResult ReadString(long offset, long rowId) + { + return ReadCellHelper(offset, rowId, length => + length > 0 + ? Encoding.Unicode.GetString(buffer, 0, length) + : string.Empty, totalLength => totalLength == 1); + } + + /// + /// Reads bytes from the file at the offset provided + /// + /// Offset into the file to read the bytes from + /// Internal ID of the row that will be stored in the cell + /// A byte array + internal FileStreamReadResult ReadBytes(long offset, long rowId) + { + return ReadCellHelper(offset, rowId, length => + { + byte[] output = new byte[length]; + Buffer.BlockCopy(buffer, 0, output, 0, length); + return output; + }, totalLength => totalLength == 1, + bytes => + { + StringBuilder sb = new StringBuilder("0x"); + foreach (byte b in bytes) + { + sb.AppendFormat("{0:X2}", b); + } + return sb.ToString(); + }); + } + + /// + /// Reads the bytes that make up a GUID at the offset provided + /// + /// Offset into the file to read the bytes from + /// Internal ID of the row that will be stored in the cell + /// A system guid type object + internal FileStreamReadResult ReadGuid(long offset, long rowId) + { + return ReadCellHelper(offset, rowId, length => + { + byte[] output = new byte[length]; + Buffer.BlockCopy(buffer, 0, output, 0, length); + return new Guid(output); + }, totalLength => totalLength == 1); + } + + /// + /// Reads a SqlMoney type from the offset provided + /// into a + /// + /// Offset into the file to read the value + /// Internal ID of the row that will be stored in the cell + /// A sql money type object + internal FileStreamReadResult ReadMoney(long offset, long rowId) + { + return ReadCellHelper(offset, rowId, length => + { + int[] arrInt32 = new int[length / 4]; + Buffer.BlockCopy(buffer, 0, arrInt32, 0, length); + return new SqlMoney(new decimal(arrInt32)); + }); + } + + /// + /// Reads the length of a field at the specified offset in the file + /// + /// Offset into the file to read the field length from + /// A LengthResult + private LengthResult ReadLength(long offset) + { + // read in length information + int lengthValue; + fileStream.Seek(offset, SeekOrigin.Begin); + int lengthLength = fileStream.Read(buffer, 0, 1); + if (buffer[0] != 0xFF) + { + // one byte is enough + lengthValue = Convert.ToInt32(buffer[0]); + } + else + { + // read in next 4 bytes + lengthLength += fileStream.Read(buffer, 0, 4); + + // reconstruct the length + lengthValue = BitConverter.ToInt32(buffer, 0); + } + + return new LengthResult { LengthLength = lengthLength, ValueLength = lengthValue }; + } + + #endregion + + /// + /// Internal struct used for representing the length of a field from the file + /// + internal struct LengthResult + { + /// + /// How many bytes the length takes up + /// + public int LengthLength { get; set; } + + /// + /// How many bytes the value takes up + /// + public int ValueLength { get; set; } + + /// + /// + + /// + public int TotalLength => LengthLength + ValueLength; + } + + #region IDisposable Implementation + + private bool disposed; + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposed) + { + return; + } + + if (disposing) + { + fileStream.Dispose(); + } + + disposed = true; + } + + ~ServiceBufferFileStreamReader() + { + Dispose(false); + } + + #endregion + + } +} diff --git a/MarkMpn.Sql4Cds.Export/DataStorage/ServiceBufferFileStreamWriter.cs b/MarkMpn.Sql4Cds.Export/DataStorage/ServiceBufferFileStreamWriter.cs new file mode 100644 index 00000000..75f0589b --- /dev/null +++ b/MarkMpn.Sql4Cds.Export/DataStorage/ServiceBufferFileStreamWriter.cs @@ -0,0 +1,580 @@ +// +// 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.Diagnostics; +using System.IO; +using System.Text; +using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using Microsoft.SqlTools.ServiceLayer.Utility; +using Microsoft.SqlTools.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage +{ + /// + /// Writer for service buffer formatted file streams + /// + public class ServiceBufferFileStreamWriter : IFileStreamWriter + { + private const int DefaultBufferLength = 8192; + + #region Member Variables + + private readonly Stream fileStream; + + private byte[] byteBuffer; + private readonly short[] shortBuffer; + private readonly int[] intBuffer; + private readonly long[] longBuffer; + private readonly char[] charBuffer; + private readonly double[] doubleBuffer; + private readonly float[] floatBuffer; + + /// + /// Functions to use for writing various types to a file + /// + private readonly Dictionary> writeMethods; + + #endregion + + /// + /// Constructs a new writer + /// + /// The file wrapper to use as the underlying file stream + /// The query execution settings + public ServiceBufferFileStreamWriter(Stream stream) + { + Validate.IsNotNull(nameof(stream), stream); + + // open file for reading/writing + if (!stream.CanWrite || !stream.CanSeek) + { + throw new InvalidOperationException("Stream must be writable and seekable."); + } + fileStream = stream; + + // create internal buffer + byteBuffer = new byte[DefaultBufferLength]; + + // Create internal buffers for blockcopy of contents to byte array + // Note: We create them now to avoid the overhead of creating a new array for every write call + shortBuffer = new short[1]; + intBuffer = new int[1]; + longBuffer = new long[1]; + charBuffer = new char[1]; + doubleBuffer = new double[1]; + floatBuffer = new float[1]; + + // Define what methods to use to write a type to the file + 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(Guid), val => WriteGuid((Guid) val)}, + + {typeof(SqlString), val => WriteNullable((SqlString) val, obj => WriteString(((SqlString) obj).Value))}, + {typeof(SqlInt16), val => WriteNullable((SqlInt16) val, obj => WriteInt16(((SqlInt16) obj).Value))}, + {typeof(SqlInt32), val => WriteNullable((SqlInt32) val, obj => WriteInt32(((SqlInt32)obj).Value))}, + {typeof(SqlInt64), val => WriteNullable((SqlInt64) val, obj => WriteInt64(((SqlInt64) obj).Value)) }, + {typeof(SqlByte), val => WriteNullable((SqlByte) val, obj => WriteByte(((SqlByte) obj).Value)) }, + {typeof(SqlBoolean), val => WriteNullable((SqlBoolean) val, obj => WriteBoolean(((SqlBoolean) obj).Value)) }, + {typeof(SqlDouble), val => WriteNullable((SqlDouble) val, obj => WriteDouble(((SqlDouble) obj).Value)) }, + {typeof(SqlSingle), val => WriteNullable((SqlSingle) val, obj => WriteSingle(((SqlSingle) obj).Value)) }, + {typeof(SqlDecimal), val => WriteNullable((SqlDecimal) val, obj => WriteSqlDecimal((SqlDecimal) obj)) }, + {typeof(SqlDateTime), val => WriteNullable((SqlDateTime) val, obj => WriteDateTime(((SqlDateTime) obj).Value)) }, + {typeof(SqlBytes), val => WriteNullable((SqlBytes) val, obj => WriteBytes(((SqlBytes) obj).Value)) }, + {typeof(SqlBinary), val => WriteNullable((SqlBinary) val, obj => WriteBytes(((SqlBinary) obj).Value)) }, + {typeof(SqlGuid), val => WriteNullable((SqlGuid) val, obj => WriteGuid(((SqlGuid) obj).Value)) }, + {typeof(SqlMoney), val => WriteNullable((SqlMoney) val, obj => WriteMoney((SqlMoney) obj)) } + }; + } + + #region IFileStreamWriter Implementation + + /// + /// Writes an entire row to the file stream + /// + /// A primed reader + /// Number of bytes used to write the row + public int WriteRow(StorageDataReader reader) + { + // Read the values in from the db + object[] values = new object[reader.Columns.Length]; + if (!reader.HasLongColumns) + { + // get all record values in one shot if there are no extra long fields + reader.GetValues(values); + } + + // Loop over all the columns and write the values to the temp file + int rowBytes = 0; + for (int i = 0; i < reader.Columns.Length; i++) + { + DbColumnWrapper ci = reader.Columns[i]; + if (reader.HasLongColumns) + { + if (reader.IsDBNull(i)) + { + // Need special case for DBNull because + // reader.GetValue doesn't return DBNull in case of SqlXml and CLR type + values[i] = DBNull.Value; + } + else + { + if (ci.IsLong.HasValue && ci.IsLong.Value) + { + // this is a long field + if (ci.IsBytes) + { + values[i] = reader.GetBytesWithMaxCapacity(i, Int32.MaxValue); + } + else if (ci.IsChars) + { + values[i] = reader.GetCharsWithMaxCapacity(i, Int32.MaxValue); + } + else if (ci.IsXml) + { + values[i] = reader.GetXmlWithMaxCapacity(i, Int32.MaxValue); + } + else + { + // we should never get here + Debug.Assert(false); + } + } + else + { + // not a long field + values[i] = reader.GetValue(i); + } + } + } + + // Get true type of the object + Type tVal = values[i] == null ? typeof(DBNull) : values[i].GetType(); + + // Write the object to a file + if (tVal == typeof(DBNull)) + { + rowBytes += WriteNull(); + } + else + { + if (ci.IsSqlVariant) + { + // serialize type information as a string before the value + string val = tVal.ToString(); + rowBytes += WriteString(val); + } + + // Use the appropriate writing method for the type + Func writeMethod; + if (writeMethods.TryGetValue(tVal, out writeMethod)) + { + rowBytes += writeMethod(values[i]); + } + else + { + rowBytes += WriteString(values[i].ToString()); + } + } + } + + // Flush the buffer after every row + FlushBuffer(); + return rowBytes; + } + + [Obsolete] + public void WriteRow(IList row, IReadOnlyList columns) + { + throw new InvalidOperationException("This type of writer is meant to write values from a DbDataReader only."); + } + + /// + /// Seeks to a given offset in the file, relative to the beginning of the file + /// + public void Seek(long offset) + { + fileStream.Seek(offset, SeekOrigin.Begin); + } + + /// + /// Flushes the internal buffer to the file stream + /// + public void FlushBuffer() + { + fileStream.Flush(); + } + + #endregion + + #region Private Helpers + + /// + /// Writes null to the file as one 0x00 byte + /// + /// Number of bytes used to store the null + internal int WriteNull() + { + byteBuffer[0] = 0x00; + return FileUtilities.WriteWithLength(fileStream, byteBuffer, 1); + } + + /// + /// Writes a short to the file + /// + /// Number of bytes used to store the short + internal int WriteInt16(short val) + { + byteBuffer[0] = 0x02; // length + shortBuffer[0] = val; + Buffer.BlockCopy(shortBuffer, 0, byteBuffer, 1, 2); + return FileUtilities.WriteWithLength(fileStream, byteBuffer, 3); + } + + /// + /// Writes a int to the file + /// + /// Number of bytes used to store the int + internal int WriteInt32(int val) + { + byteBuffer[0] = 0x04; // length + intBuffer[0] = val; + Buffer.BlockCopy(intBuffer, 0, byteBuffer, 1, 4); + return FileUtilities.WriteWithLength(fileStream, byteBuffer, 5); + } + + /// + /// Writes a long to the file + /// + /// Number of bytes used to store the long + internal int WriteInt64(long val) + { + byteBuffer[0] = 0x08; // length + longBuffer[0] = val; + Buffer.BlockCopy(longBuffer, 0, byteBuffer, 1, 8); + return FileUtilities.WriteWithLength(fileStream, byteBuffer, 9); + } + + /// + /// Writes a char to the file + /// + /// Number of bytes used to store the char + internal int WriteChar(char val) + { + byteBuffer[0] = 0x02; // length + charBuffer[0] = val; + Buffer.BlockCopy(charBuffer, 0, byteBuffer, 1, 2); + return FileUtilities.WriteWithLength(fileStream, byteBuffer, 3); + } + + /// + /// Writes a bool to the file + /// + /// Number of bytes used to store the bool + internal int WriteBoolean(bool val) + { + byteBuffer[0] = 0x01; // length + byteBuffer[1] = (byte)(val ? 0x01 : 0x00); + return FileUtilities.WriteWithLength(fileStream, byteBuffer, 2); + } + + /// + /// Writes a byte to the file + /// + /// Number of bytes used to store the byte + internal int WriteByte(byte val) + { + byteBuffer[0] = 0x01; // length + byteBuffer[1] = val; + return FileUtilities.WriteWithLength(fileStream, byteBuffer, 2); + } + + /// + /// Writes a float to the file + /// + /// Number of bytes used to store the float + internal int WriteSingle(float val) + { + byteBuffer[0] = 0x04; // length + floatBuffer[0] = val; + Buffer.BlockCopy(floatBuffer, 0, byteBuffer, 1, 4); + return FileUtilities.WriteWithLength(fileStream, byteBuffer, 5); + } + + /// + /// Writes a double to the file + /// + /// Number of bytes used to store the double + internal int WriteDouble(double val) + { + byteBuffer[0] = 0x08; // length + doubleBuffer[0] = val; + Buffer.BlockCopy(doubleBuffer, 0, byteBuffer, 1, 8); + return FileUtilities.WriteWithLength(fileStream, byteBuffer, 9); + } + + /// + /// Writes a SqlDecimal to the file + /// + /// Number of bytes used to store the SqlDecimal + internal int WriteSqlDecimal(SqlDecimal val) + { + int[] arrInt32 = val.Data; + int iLen = 3 + (arrInt32.Length * 4); + int iTotalLen = WriteLength(iLen); // length + + // precision + byteBuffer[0] = val.Precision; + + // scale + byteBuffer[1] = val.Scale; + + // positive + byteBuffer[2] = (byte)(val.IsPositive ? 0x01 : 0x00); + + // data value + Buffer.BlockCopy(arrInt32, 0, byteBuffer, 3, iLen - 3); + iTotalLen += FileUtilities.WriteWithLength(fileStream, byteBuffer, iLen); + return iTotalLen; // len+data + } + + /// + /// Writes a decimal to the file + /// + /// Number of bytes used to store the decimal + internal int WriteDecimal(decimal val) + { + int[] arrInt32 = decimal.GetBits(val); + + int iLen = arrInt32.Length * 4; + int iTotalLen = WriteLength(iLen); // length + + Buffer.BlockCopy(arrInt32, 0, byteBuffer, 0, iLen); + iTotalLen += FileUtilities.WriteWithLength(fileStream, byteBuffer, iLen); + + return iTotalLen; // len+data + } + + /// + /// Writes a DateTime to the file + /// + /// Number of bytes used to store the DateTime + public int WriteDateTime(DateTime dtVal) + { + return WriteInt64(dtVal.Ticks); + } + + /// + /// Writes a DateTimeOffset to the file + /// + /// Number of bytes used to store the DateTimeOffset + internal int WriteDateTimeOffset(DateTimeOffset dtoVal) + { + // Write the length, which is the 2*sizeof(long) + byteBuffer[0] = 0x10; // length (16) + + // Write the two longs, the datetime and the offset + long[] longBufferOffset = new long[2]; + longBufferOffset[0] = dtoVal.Ticks; + longBufferOffset[1] = dtoVal.Offset.Ticks; + Buffer.BlockCopy(longBufferOffset, 0, byteBuffer, 1, 16); + return FileUtilities.WriteWithLength(fileStream, byteBuffer, 17); + } + + /// + /// Writes a TimeSpan to the file + /// + /// Number of bytes used to store the TimeSpan + internal int WriteTimeSpan(TimeSpan timeSpan) + { + return WriteInt64(timeSpan.Ticks); + } + + /// + /// Writes a string to the file + /// + /// Number of bytes used to store the string + internal int WriteString(string sVal) + { + Validate.IsNotNull(nameof(sVal), sVal); + + int iTotalLen; + if (0 == sVal.Length) // special case of 0 length string + { + const int iLen = 5; + + AssureBufferLength(iLen); + byteBuffer[0] = 0xFF; + byteBuffer[1] = 0x00; + byteBuffer[2] = 0x00; + byteBuffer[3] = 0x00; + byteBuffer[4] = 0x00; + + iTotalLen = FileUtilities.WriteWithLength(fileStream, byteBuffer, 5); + } + else + { + // Convert to a unicode byte array + byte[] bytes = Encoding.Unicode.GetBytes(sVal); + + // convert char array into byte array and write it out + iTotalLen = WriteLength(bytes.Length); + iTotalLen += FileUtilities.WriteWithLength(fileStream, bytes, bytes.Length); + } + return iTotalLen; // len+data + } + + /// + /// Writes a byte[] to the file + /// + /// Number of bytes used to store the byte[] + internal int WriteBytes(byte[] bytesVal) + { + Validate.IsNotNull(nameof(bytesVal), bytesVal); + + int iTotalLen; + if (bytesVal.Length == 0) // special case of 0 length byte array "0x" + { + AssureBufferLength(5); + byteBuffer[0] = 0xFF; + byteBuffer[1] = 0x00; + byteBuffer[2] = 0x00; + byteBuffer[3] = 0x00; + byteBuffer[4] = 0x00; + + iTotalLen = FileUtilities.WriteWithLength(fileStream, byteBuffer, 5); + } + else + { + iTotalLen = WriteLength(bytesVal.Length); + iTotalLen += FileUtilities.WriteWithLength(fileStream, bytesVal, bytesVal.Length); + } + return iTotalLen; // len+data + } + + /// + /// Stores a GUID value to the file by treating it as a byte array + /// + /// The GUID to write to the file + /// Number of bytes written to the file + internal int WriteGuid(Guid val) + { + byte[] guidBytes = val.ToByteArray(); + return WriteBytes(guidBytes); + } + + /// + /// Stores a SqlMoney value to the file by treating it as a decimal + /// + /// The SqlMoney value to write to the file + /// Number of bytes written to the file + internal int WriteMoney(SqlMoney val) + { + return WriteDecimal(val.Value); + } + + /// + /// Creates a new buffer that is of the specified length if the buffer is not already + /// at least as long as specified. + /// + /// The minimum buffer size + private void AssureBufferLength(int newBufferLength) + { + if (newBufferLength > byteBuffer.Length) + { + byteBuffer = new byte[byteBuffer.Length]; + } + } + + /// + /// Writes the length of the field using the appropriate number of bytes (ie, 1 if the + /// length is <255, 5 if the length is >=255) + /// + /// Number of bytes used to store the length + private int WriteLength(int iLen) + { + if (iLen < 0xFF) + { + // fits in one byte of memory only need to write one byte + int iTmp = iLen & 0x000000FF; + + byteBuffer[0] = Convert.ToByte(iTmp); + return FileUtilities.WriteWithLength(fileStream, byteBuffer, 1); + } + // The length won't fit in 1 byte, so we need to use 1 byte to signify that the length + // is a full 4 bytes. + byteBuffer[0] = 0xFF; + + // convert int32 into array of bytes + intBuffer[0] = iLen; + Buffer.BlockCopy(intBuffer, 0, byteBuffer, 1, 4); + return FileUtilities.WriteWithLength(fileStream, byteBuffer, 5); + } + + /// + /// Writes a Nullable type (generally a Sql* type) to the file. The function provided by + /// is used to write to the file if + /// is not null. is used if is null. + /// + /// The value to write to the file + /// The function to use if val is not null + /// Number of bytes used to write value to the file + private int WriteNullable(INullable val, Func valueWriteFunc) + { + return val.IsNull ? WriteNull() : valueWriteFunc(val); + } + + #endregion + + #region IDisposable Implementation + + private bool disposed; + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposed) + { + return; + } + + if (disposing) + { + fileStream.Flush(); + fileStream.Dispose(); + } + + disposed = true; + } + + ~ServiceBufferFileStreamWriter() + { + Dispose(false); + } + + #endregion + } +} diff --git a/MarkMpn.Sql4Cds.Export/DataStorage/StorageDataReader.cs b/MarkMpn.Sql4Cds.Export/DataStorage/StorageDataReader.cs new file mode 100644 index 00000000..4677f90d --- /dev/null +++ b/MarkMpn.Sql4Cds.Export/DataStorage/StorageDataReader.cs @@ -0,0 +1,343 @@ +// +// 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.Data.Common; +using System.Data.SqlTypes; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Xml; +using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using Microsoft.SqlTools.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage +{ + /// + /// Wrapper around a DbData reader to perform some special operations more simply + /// + public class StorageDataReader + { + // This code is based on code from Microsoft.SqlServer.Management.UI.Grid, SSMS DataStorage, + // StorageDataReader + // $\Data Tools\SSMS_XPlat\sql\ssms\core\DataStorage\src\StorageDataReader.cs + + /// + /// Constructs a new wrapper around the provided reader + /// + /// The reader to wrap around + public StorageDataReader(DbDataReader reader) + { + // Sanity check to make sure there is a data reader + Validate.IsNotNull(nameof(reader), reader); + + DbDataReader = reader; + + // Read the columns into a set of wrappers + Columns = DbDataReader.GetColumnSchema().Select(column => new DbColumnWrapper(column)).ToArray(); + HasLongColumns = Columns.Any(column => column.IsLong.HasValue && column.IsLong.Value); + } + + #region Properties + + /// + /// All the columns that this reader currently contains + /// + public DbColumnWrapper[] Columns { get; private set; } + + /// + /// The that will be read from + /// + public DbDataReader DbDataReader { get; private set; } + + /// + /// Whether or not any of the columns of this reader are 'long', such as nvarchar(max) + /// + public bool HasLongColumns { get; private set; } + + #endregion + + #region DbDataReader Methods + + /// + /// Pass-through to DbDataReader.Read() + /// + /// ************** IMPORTANT **************** + /// M.D.SqlClient's ReadAsync() implementation is not as + /// performant as Read() and doesn't respect Cancellation Token + /// due to long existing design issues like below: + /// + /// https://github.com/dotnet/SqlClient/issues/593 + /// https://github.com/dotnet/SqlClient/issues/44 + /// + /// Until these issues are resolved, prefer using Sync APIs. + /// ***************************************** + /// + /// + /// + public bool Read() + { + return DbDataReader.Read(); + } + + /// + /// Retrieves a value + /// + /// Column ordinal + /// The value of the given column + public object GetValue(int i) + { + return DbDataReader.GetProviderSpecificValue(i); + } + + /// + /// Stores all values of the current row into the provided object array + /// + /// Where to store the values from this row + public void GetValues(object[] values) + { + DbDataReader.GetProviderSpecificValues(values); + } + + /// + /// Whether or not the cell of the given column at the current row is a DBNull + /// + /// Column ordinal + /// True if the cell is DBNull, false otherwise + public bool IsDBNull(int i) + { + return DbDataReader.IsDBNull(i); + } + + #endregion + + #region Public Methods + + /// + /// Retrieves bytes with a maximum number of bytes to return + /// + /// Column ordinal + /// Number of bytes to return at maximum + /// Byte array + public byte[] GetBytesWithMaxCapacity(int iCol, int maxNumBytesToReturn) + { + if (maxNumBytesToReturn <= 0) + { + throw new ArgumentOutOfRangeException(nameof(maxNumBytesToReturn), SR.QueryServiceDataReaderByteCountInvalid); + } + + //first, ask provider how much data it has and calculate the final # of bytes + //NOTE: -1 means that it doesn't know how much data it has + long neededLength; + long origLength = neededLength = GetBytes(iCol, 0, null, 0, 0); + if (neededLength == -1 || neededLength > maxNumBytesToReturn) + { + neededLength = maxNumBytesToReturn; + } + + //get the data up to the maxNumBytesToReturn + byte[] bytesBuffer = new byte[neededLength]; + GetBytes(iCol, 0, bytesBuffer, 0, (int)neededLength); + + //see if server sent back more data than we should return + if (origLength == -1 || origLength > neededLength) + { + //pump the rest of data from the reader and discard it right away + long dataIndex = neededLength; + const int tmpBufSize = 100000; + byte[] tmpBuf = new byte[tmpBufSize]; + while (GetBytes(iCol, dataIndex, tmpBuf, 0, tmpBufSize) == tmpBufSize) + { + dataIndex += tmpBufSize; + } + } + + return bytesBuffer; + } + + /// + /// Retrieves characters with a maximum number of charss to return + /// + /// Column ordinal + /// Number of chars to return at maximum + /// String + public string GetCharsWithMaxCapacity(int iCol, int maxCharsToReturn) + { + if (maxCharsToReturn <= 0) + { + throw new ArgumentOutOfRangeException(nameof(maxCharsToReturn), SR.QueryServiceDataReaderCharCountInvalid); + } + + //first, ask provider how much data it has and calculate the final # of chars + //NOTE: -1 means that it doesn't know how much data it has + long neededLength; + long origLength = neededLength = GetChars(iCol, 0, null, 0, 0); + if (neededLength == -1 || neededLength > maxCharsToReturn) + { + neededLength = maxCharsToReturn; + } + Debug.Assert(neededLength < int.MaxValue); + + //get the data up to maxCharsToReturn + char[] buffer = new char[neededLength]; + if (neededLength > 0) + { + GetChars(iCol, 0, buffer, 0, (int)neededLength); + } + + //see if server sent back more data than we should return + if (origLength == -1 || origLength > neededLength) + { + //pump the rest of data from the reader and discard it right away + long dataIndex = neededLength; + const int tmpBufSize = 100000; + char[] tmpBuf = new char[tmpBufSize]; + while (GetChars(iCol, dataIndex, tmpBuf, 0, tmpBufSize) == tmpBufSize) + { + dataIndex += tmpBufSize; + } + } + string res = new string(buffer); + return res; + } + + /// + /// Retrieves xml with a maximum number of bytes to return + /// + /// Column ordinal + /// Number of chars to return at maximum + /// String + public string GetXmlWithMaxCapacity(int iCol, int maxCharsToReturn) + { + if (maxCharsToReturn <= 0) + { + throw new ArgumentOutOfRangeException(nameof(maxCharsToReturn), SR.QueryServiceDataReaderXmlCountInvalid); + } + + // We have SQL XML support, so write it properly + SqlXml sm = GetSqlXml(iCol); + if (sm == null) + { + return null; + } + + // Setup the writer so that we don't close the memory stream and can process fragments + // of XML + XmlWriterSettings writerSettings = new XmlWriterSettings + { + CloseOutput = false, // don't close the memory stream + ConformanceLevel = ConformanceLevel.Fragment + }; + + using (StringWriterWithMaxCapacity sw = new StringWriterWithMaxCapacity(null, maxCharsToReturn)) + using (XmlWriter ww = XmlWriter.Create(sw, writerSettings)) + using (XmlReader reader = sm.CreateReader()) + { + reader.Read(); + + while (!reader.EOF) + { + ww.WriteNode(reader, true); + } + + ww.Flush(); + return sw.ToString(); + } + } + + #endregion + + #region Private Helpers + + private long GetBytes(int i, long dataIndex, byte[] buffer, int bufferIndex, int length) + { + return DbDataReader.GetBytes(i, dataIndex, buffer, bufferIndex, length); + } + + private long GetChars(int i, long dataIndex, char[] buffer, int bufferIndex, int length) + { + return DbDataReader.GetChars(i, dataIndex, buffer, bufferIndex, length); + } + + private SqlXml GetSqlXml(int i) + { + return (SqlXml)DbDataReader.GetProviderSpecificValue(i); + } + + #endregion + + /// + /// Internal class for writing strings with a maximum capacity + /// + /// + /// This code is take almost verbatim from Microsoft.SqlServer.Management.UI.Grid, SSMS + /// DataStorage, StorageDataReader class. + /// + internal class StringWriterWithMaxCapacity : StringWriter + { + private bool stopWriting; + + private int CurrentLength + { + get { return GetStringBuilder().Length; } + } + + public StringWriterWithMaxCapacity(IFormatProvider formatProvider, int capacity) : base(formatProvider) + { + MaximumCapacity = capacity; + } + + private int MaximumCapacity { get; set; } + + public override void Write(char value) + { + if (stopWriting) { return; } + + if (CurrentLength < MaximumCapacity) + { + base.Write(value); + } + else + { + stopWriting = true; + } + } + + public override void Write(char[] buffer, int index, int count) + { + if (stopWriting) { return; } + + int curLen = CurrentLength; + if (curLen + (count - index) > MaximumCapacity) + { + stopWriting = true; + + count = MaximumCapacity - curLen + index; + if (count < 0) + { + count = 0; + } + } + base.Write(buffer, index, count); + } + + public override void Write(string value) + { + if (stopWriting) { return; } + + int curLen = CurrentLength; + if (value.Length + curLen > MaximumCapacity) + { + stopWriting = true; + base.Write(value.Substring(0, MaximumCapacity - curLen)); + } + else + { + base.Write(value); + } + } + } + } +} diff --git a/MarkMpn.Sql4Cds.Export/MarkMpn.Sql4Cds.Export.csproj b/MarkMpn.Sql4Cds.Export/MarkMpn.Sql4Cds.Export.csproj new file mode 100644 index 00000000..717d905a --- /dev/null +++ b/MarkMpn.Sql4Cds.Export/MarkMpn.Sql4Cds.Export.csproj @@ -0,0 +1,18 @@ + + + + netstandard2.0 + + + + + + + + + + + + + + diff --git a/MarkMpn.Sql4Cds.Export/README.md b/MarkMpn.Sql4Cds.Export/README.md new file mode 100644 index 00000000..486f93c6 --- /dev/null +++ b/MarkMpn.Sql4Cds.Export/README.md @@ -0,0 +1,2 @@ +A fork of various classes from https://github.com/microsoft/sqltoolsservice to handle storing and exporting +data from a data reader to a file in various different formats. \ No newline at end of file diff --git a/MarkMpn.Sql4Cds.Export/SR.cs b/MarkMpn.Sql4Cds.Export/SR.cs new file mode 100644 index 00000000..f9955f58 --- /dev/null +++ b/MarkMpn.Sql4Cds.Export/SR.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using System.Text; + +namespace Microsoft.SqlTools.ServiceLayer.QueryExecution +{ + internal class SR + { + public static string QueryServiceColumnNull => "(No column name)"; + public static string QueryServiceCellNull => "NULL"; + public static string QueryServiceDataReaderByteCountInvalid { get; internal set; } + public static string QueryServiceDataReaderCharCountInvalid { get; internal set; } + public static string QueryServiceDataReaderXmlCountInvalid { get; internal set; } + + internal static string QueryServiceUnsupportedSqlVariantType(string sqlVariantType, string columnName) + { + throw new NotImplementedException(); + } + } +} diff --git a/MarkMpn.Sql4Cds.Export/Utility/Extensions.cs b/MarkMpn.Sql4Cds.Export/Utility/Extensions.cs new file mode 100644 index 00000000..70daac58 --- /dev/null +++ b/MarkMpn.Sql4Cds.Export/Utility/Extensions.cs @@ -0,0 +1,99 @@ +// +// 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; + +namespace Microsoft.SqlTools.Utility +{ + public static class ObjectExtensions + { + /// + /// Extension to evaluate an object's ToString() method in an exception safe way. This will + /// extension method will not throw. + /// + /// The object on which to call ToString() + /// The ToString() return value or a suitable error message is that throws. + public static string SafeToString(this object obj) + { + string str; + + try + { + str = obj.ToString(); + } + catch (Exception ex) + { + str = $""; + } + + return str; + } + + /// + /// Converts a boolean to a "1" or "0" string. Particularly helpful when sending telemetry + /// + public static string ToOneOrZeroString(this bool isTrue) + { + return isTrue ? "1" : "0"; + } + } + + public static class NullableExtensions + { + /// + /// Extension method to evaluate a bool? and determine if it has the value and is true. + /// This way we avoid throwing if the bool? doesn't have a value. + /// + /// The bool? to process + /// + /// true if has a value and it is true + /// false otherwise. + /// + public static bool HasTrue(this bool? obj) + { + return obj.HasValue && obj.Value; + } + } + + public static class ExceptionExtensions + { + /// + /// Returns true if the passed exception or any inner exception is an OperationCanceledException instance. + /// + public static bool IsOperationCanceledException(this Exception e) + { + Exception current = e; + while (current != null) + { + if (current is OperationCanceledException) + { + return true; + } + + current = current.InnerException; + } + + return false; + } + + public static string GetFullErrorMessage(this Exception e, bool includeStackTrace = false) + { + List errors = new List(); + + while (e != null) + { + errors.Add(e.Message); + if (includeStackTrace) + { + errors.Add(e.StackTrace); + } + e = e.InnerException; + } + + return errors.Count > 0 ? string.Join(includeStackTrace ? Environment.NewLine : " ---> ", errors) : string.Empty; + } + } +} diff --git a/MarkMpn.Sql4Cds.Export/Utility/FileUtils.cs b/MarkMpn.Sql4Cds.Export/Utility/FileUtils.cs new file mode 100644 index 00000000..66f7986c --- /dev/null +++ b/MarkMpn.Sql4Cds.Export/Utility/FileUtils.cs @@ -0,0 +1,144 @@ +// +// 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.IO; + +namespace Microsoft.SqlTools.ServiceLayer.Utility +{ + internal static class FileUtilities + { + internal static string PeekDefinitionTempFolder = Path.GetTempPath() + "mssql_definition"; + internal static string AgentNotebookTempFolder = Path.GetTempPath() + "mssql_notebooks"; + internal static bool PeekDefinitionTempFolderCreated = false; + + internal static string GetPeekDefinitionTempFolder() + { + string tempPath; + if (!PeekDefinitionTempFolderCreated) + { + try + { + // create new temp folder + string tempFolder = string.Format("{0}_{1}", FileUtilities.PeekDefinitionTempFolder, DateTime.Now.ToString("yyyyMMddHHmmssffff")); + DirectoryInfo tempScriptDirectory = Directory.CreateDirectory(tempFolder); + FileUtilities.PeekDefinitionTempFolder = tempScriptDirectory.FullName; + tempPath = tempScriptDirectory.FullName; + PeekDefinitionTempFolderCreated = true; + } + catch (Exception) + { + // swallow exception and use temp folder to store scripts + tempPath = Path.GetTempPath(); + } + } + else + { + try + { + // use tempDirectory name created previously + DirectoryInfo tempScriptDirectory = Directory.CreateDirectory(FileUtilities.PeekDefinitionTempFolder); + tempPath = tempScriptDirectory.FullName; + } + catch (Exception) + { + // swallow exception and use temp folder to store scripts + tempPath = Path.GetTempPath(); + } + } + return tempPath; + } + + /// + /// Checks if file exists and swallows exceptions, if any + /// + /// path of the file + /// + internal static bool SafeFileExists(string path) + { + try + { + return File.Exists(path); + } + catch (Exception) + { + // Swallow exception + return false; + } + } + + /// + /// Deletes a file and swallows exceptions, if any + /// + /// + internal static void SafeFileDelete(string path) + { + try + { + File.Delete(path); + } + catch (Exception) + { + // Swallow exception, do nothing + } + } + + internal static int WriteWithLength(Stream stream, byte[] buffer, int length) + { + stream.Write(buffer, 0, length); + return length; + } + + /// + /// Checks if file exists and swallows exceptions, if any + /// + /// path of the file + /// + internal static bool SafeDirectoryExists(string path) + { + try + { + return Directory.Exists(path); + } + catch (Exception) + { + // Swallow exception + return false; + } + } + + + /// + /// Deletes a directory and swallows exceptions, if any + /// + /// + internal static void SafeDirectoryDelete(string path, bool recursive) + { + try + { + Directory.Delete(path, recursive); + } + catch (Exception) + { + // Swallow exception, do nothing + } + } + + /// + /// Turns off the read-only attribute for this file + /// + /// + + internal static void SetFileReadWrite(string fullFilePath) + { + if (!string.IsNullOrEmpty(fullFilePath) && + File.Exists(fullFilePath)) + { + File.SetAttributes(fullFilePath, FileAttributes.Normal); + } + } + + } +} \ No newline at end of file diff --git a/MarkMpn.Sql4Cds.Export/Utility/Validate.cs b/MarkMpn.Sql4Cds.Export/Utility/Validate.cs new file mode 100644 index 00000000..1b3d4074 --- /dev/null +++ b/MarkMpn.Sql4Cds.Export/Utility/Validate.cs @@ -0,0 +1,158 @@ +// +// 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; + +namespace Microsoft.SqlTools.Utility +{ + /// + /// Provides common validation methods to simplify method + /// parameter checks. + /// + public static class Validate + { + /// + /// Throws ArgumentNullException if value is null. + /// + /// The name of the parameter being validated. + /// The value of the parameter being validated. + public static void IsNotNull(string parameterName, object valueToCheck) + { + if (valueToCheck == null) + { + throw new ArgumentNullException(parameterName); + } + } + + /// + /// Throws ArgumentOutOfRangeException if the value is outside + /// of the given lower and upper limits. + /// + /// The name of the parameter being validated. + /// The value of the parameter being validated. + /// The lower limit which the value should not be less than. + /// The upper limit which the value should not be greater than. + public static void IsWithinRange( + string parameterName, + long valueToCheck, + long lowerLimit, + long upperLimit) + { + // TODO: Debug assert here if lowerLimit >= upperLimit + + if (valueToCheck < lowerLimit || valueToCheck > upperLimit) + { + throw new ArgumentOutOfRangeException( + parameterName, + valueToCheck, + string.Format( + "Value is not between {0} and {1}", + lowerLimit, + upperLimit)); + } + } + + /// + /// Throws ArgumentOutOfRangeException if the value is greater than or equal + /// to the given upper limit. + /// + /// The name of the parameter being validated. + /// The value of the parameter being validated. + /// The upper limit which the value should be less than. + public static void IsLessThan( + string parameterName, + long valueToCheck, + long upperLimit) + { + if (valueToCheck >= upperLimit) + { + throw new ArgumentOutOfRangeException( + parameterName, + valueToCheck, + string.Format( + "Value is greater than or equal to {0}", + upperLimit)); + } + } + + /// + /// Throws ArgumentOutOfRangeException if the value is less than or equal + /// to the given lower limit. + /// + /// The name of the parameter being validated. + /// The value of the parameter being validated. + /// The lower limit which the value should be greater than. + public static void IsGreaterThan( + string parameterName, + long valueToCheck, + long lowerLimit) + { + if (valueToCheck < lowerLimit) + { + throw new ArgumentOutOfRangeException( + parameterName, + valueToCheck, + string.Format( + "Value is less than or equal to {0}", + lowerLimit)); + } + } + + /// + /// Throws ArgumentException if the value is equal to the undesired value. + /// + /// The type of value to be validated. + /// The name of the parameter being validated. + /// The value that valueToCheck should not equal. + /// The value of the parameter being validated. + public static void IsNotEqual( + string parameterName, + TValue valueToCheck, + TValue undesiredValue) + { + if (EqualityComparer.Default.Equals(valueToCheck, undesiredValue)) + { + throw new ArgumentException( + string.Format( + "The given value '{0}' should not equal '{1}'", + valueToCheck, + undesiredValue), + parameterName); + } + } + + /// + /// Throws ArgumentException if the value is null or an empty string. + /// + /// The name of the parameter being validated. + /// The value of the parameter being validated. + public static void IsNotNullOrEmptyString(string parameterName, string valueToCheck) + { + if (string.IsNullOrEmpty(valueToCheck)) + { + throw new ArgumentException( + "Parameter contains a null, empty, or whitespace string.", + parameterName); + } + } + + /// + /// Throws ArgumentException if the value is null, an empty string, + /// or a string containing only whitespace. + /// + /// The name of the parameter being validated. + /// The value of the parameter being validated. + public static void IsNotNullOrWhitespaceString(string parameterName, string valueToCheck) + { + if (string.IsNullOrWhiteSpace(valueToCheck)) + { + throw new ArgumentException( + "Parameter contains a null, empty, or whitespace string.", + parameterName); + } + } + } +} diff --git a/MarkMpn.Sql4Cds.Export/ValueFormatter.cs b/MarkMpn.Sql4Cds.Export/ValueFormatter.cs new file mode 100644 index 00000000..130efb2e --- /dev/null +++ b/MarkMpn.Sql4Cds.Export/ValueFormatter.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Data.SqlTypes; +using System.Text; +using Microsoft.SqlTools.ServiceLayer.QueryExecution; +using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; + +namespace MarkMpn.Sql4Cds.Export +{ + public static class ValueFormatter + { + public static DbCellValue Format(object value, string dataTypeName, int? numericScale, bool localFormatDates) + { + if (value == null || value is DBNull || value is INullable nullable && nullable.IsNull) + { + return new DbCellValue + { + DisplayValue = SR.QueryServiceCellNull, + IsNull = true + }; + } + + var text = value?.ToString(); + + if (value is bool b) + { + text = b ? "1" : "0"; + } + else if (value is DateTime dt) + { + if (dataTypeName == "date") + { + if (localFormatDates) + text = dt.ToShortDateString(); + else + text = dt.ToString("yyyy-MM-dd"); + } + else if (dataTypeName == "smalldatetime") + { + if (localFormatDates) + text = dt.ToShortDateString() + " " + dt.ToString("HH:mm"); + else + text = dt.ToString("yyyy-MM-dd HH:mm"); + } + else if (!localFormatDates) + { + text = dt.ToString("yyyy-MM-dd HH:mm:ss" + (numericScale == 0 ? "" : ("." + new string('f', numericScale.Value)))); + } + } + else if (value is TimeSpan ts && !localFormatDates) + { + text = ts.ToString("hh\\:mm\\:ss" + (numericScale == 0 ? "" : ("\\." + new string('f', numericScale.Value)))); + } + else if (value is decimal dec) + { + text = dec.ToString("0" + (numericScale == 0 ? "" : ("." + new string('0', numericScale.Value)))); + } + + return new DbCellValue + { + DisplayValue = text, + InvariantCultureDisplayValue = text, + RawObject = value, + }; + } + } +} diff --git a/MarkMpn.Sql4Cds.LanguageServer/MarkMpn.Sql4Cds.LanguageServer.csproj b/MarkMpn.Sql4Cds.LanguageServer/MarkMpn.Sql4Cds.LanguageServer.csproj index cd84ce12..d2292c7a 100644 --- a/MarkMpn.Sql4Cds.LanguageServer/MarkMpn.Sql4Cds.LanguageServer.csproj +++ b/MarkMpn.Sql4Cds.LanguageServer/MarkMpn.Sql4Cds.LanguageServer.csproj @@ -17,6 +17,7 @@ + diff --git a/MarkMpn.Sql4Cds.LanguageServer/QueryExecution/Contracts/ResultSetSubset.cs b/MarkMpn.Sql4Cds.LanguageServer/QueryExecution/Contracts/ResultSetSubset.cs index 506852e0..8ead94cd 100644 --- a/MarkMpn.Sql4Cds.LanguageServer/QueryExecution/Contracts/ResultSetSubset.cs +++ b/MarkMpn.Sql4Cds.LanguageServer/QueryExecution/Contracts/ResultSetSubset.cs @@ -1,4 +1,6 @@ -namespace MarkMpn.Sql4Cds.LanguageServer.QueryExecution.Contracts +using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; + +namespace MarkMpn.Sql4Cds.LanguageServer.QueryExecution.Contracts { public class ResultSetSubset { diff --git a/MarkMpn.Sql4Cds.LanguageServer/QueryExecution/Contracts/ResultSetSummary.cs b/MarkMpn.Sql4Cds.LanguageServer/QueryExecution/Contracts/ResultSetSummary.cs index 5fe11524..c0ed6bf0 100644 --- a/MarkMpn.Sql4Cds.LanguageServer/QueryExecution/Contracts/ResultSetSummary.cs +++ b/MarkMpn.Sql4Cds.LanguageServer/QueryExecution/Contracts/ResultSetSummary.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; namespace MarkMpn.Sql4Cds.LanguageServer.QueryExecution.Contracts { diff --git a/MarkMpn.Sql4Cds.LanguageServer/QueryExecution/QueryExecutionHandler.cs b/MarkMpn.Sql4Cds.LanguageServer/QueryExecution/QueryExecutionHandler.cs index 15aec57f..a3c49a28 100644 --- a/MarkMpn.Sql4Cds.LanguageServer/QueryExecution/QueryExecutionHandler.cs +++ b/MarkMpn.Sql4Cds.LanguageServer/QueryExecution/QueryExecutionHandler.cs @@ -14,11 +14,13 @@ using System.Threading.Tasks; using MarkMpn.Sql4Cds.Engine; using MarkMpn.Sql4Cds.Engine.ExecutionPlan; +using MarkMpn.Sql4Cds.Export; using MarkMpn.Sql4Cds.LanguageServer.Configuration; using MarkMpn.Sql4Cds.LanguageServer.Connection; using MarkMpn.Sql4Cds.LanguageServer.QueryExecution.Contracts; using MarkMpn.Sql4Cds.LanguageServer.Workspace; using Microsoft.SqlTools.ServiceLayer.ExecutionPlan.Contracts; +using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; using Microsoft.VisualStudio.LanguageServer.Protocol; using Microsoft.Xrm.Sdk; using Microsoft.Xrm.Sdk.Metadata; @@ -299,7 +301,7 @@ private async Task ExecuteAsync(Connection.Session session, ExecuteRequestParams Id = resultSets.Count, BatchId = batchSummary.Id, Complete = true, - ColumnInfo = new[] { new DbColumnWrapper(new ColumnInfo("Microsoft SQL Server 2005 XML Showplan", "xml", 0)) }, + ColumnInfo = new[] { new DbColumnWrapper("Microsoft SQL Server 2005 XML Showplan", "xml", null) }, RowCount = 0, SpecialAction = new SpecialAction { ExpectYukonXMLShowPlan = true }, }; @@ -341,10 +343,7 @@ private async Task ExecuteAsync(Connection.Session session, ExecuteRequestParams var schemaTable = reader.GetSchemaTable(); for (var i = 0; i < reader.FieldCount; i++) - resultSet.ColumnInfo[i] = new DbColumnWrapper(new ColumnInfo( - String.IsNullOrEmpty(reader.GetName(i)) ? $"(No column name)" : reader.GetName(i), - reader.GetDataTypeName(i), - (short)schemaTable.Rows[i]["NumericScale"])); + resultSet.ColumnInfo[i] = new DbColumnWrapper(reader.GetName(i), reader.GetDataTypeName(i), null); resultSetInProgress = resultSet; resultSets.Add(resultSet); @@ -390,7 +389,7 @@ private async Task ExecuteAsync(Connection.Session session, ExecuteRequestParams Id = resultSets.Count, BatchId = batchSummary.Id, Complete = true, - ColumnInfo = new[] { new DbColumnWrapper(new ColumnInfo("Microsoft SQL Server 2005 XML Showplan", "xml", 0)) }, + ColumnInfo = new[] { new DbColumnWrapper("Microsoft SQL Server 2005 XML Showplan", "xml", null) }, RowCount = 0, SpecialAction = new SpecialAction { ExpectYukonXMLShowPlan = true }, }; @@ -1007,54 +1006,7 @@ public SubsetResult HandleSubset(SubsetParams request) .Select((value, colIndex) => { var col = resultSet.ColumnInfo[colIndex]; - var text = value?.ToString(); - - if (value is bool b) - { - text = b ? "1" : "0"; - } - else if (value is DateTime dt) - { - var type = col.DataTypeName; - - if (type == "date") - { - if (Sql4CdsSettings.Instance.LocalFormatDates) - text = dt.ToShortDateString(); - else - text = dt.ToString("yyyy-MM-dd"); - } - else if (type == "smalldatetime") - { - if (Sql4CdsSettings.Instance.LocalFormatDates) - text = dt.ToShortDateString() + " " + dt.ToString("HH:mm"); - else - text = dt.ToString("yyyy-MM-dd HH:mm"); - } - else if (!Sql4CdsSettings.Instance.LocalFormatDates) - { - var scale = col.NumericScale.Value; - text = dt.ToString("yyyy-MM-dd HH:mm:ss" + (scale == 0 ? "" : ("." + new string('f', scale)))); - } - } - else if (value is TimeSpan ts && !Sql4CdsSettings.Instance.LocalFormatDates) - { - var scale = col.NumericScale.Value; - text = ts.ToString("hh\\:mm\\:ss" + (scale == 0 ? "" : ("\\." + new string('f', scale)))); - } - else if (value is decimal dec) - { - var scale = col.NumericScale.Value; - text = dec.ToString("0" + (scale == 0 ? "" : ("." + new string('0', scale)))); - } - - return new DbCellValue - { - DisplayValue = text, - InvariantCultureDisplayValue = text, - IsNull = value == null || value.Equals(DBNull.Value), - RawObject = value, - }; + return ValueFormatter.Format(value, col.DataTypeName, col.NumericScale.GetValueOrDefault(), Sql4CdsSettings.Instance.LocalFormatDates); }) .ToArray()) .ToArray() diff --git a/MarkMpn.Sql4Cds.XTB/MarkMpn.Sql4Cds.XTB.csproj b/MarkMpn.Sql4Cds.XTB/MarkMpn.Sql4Cds.XTB.csproj index 24a849f0..eba275f6 100644 --- a/MarkMpn.Sql4Cds.XTB/MarkMpn.Sql4Cds.XTB.csproj +++ b/MarkMpn.Sql4Cds.XTB/MarkMpn.Sql4Cds.XTB.csproj @@ -295,6 +295,10 @@ {23288bb2-0d6f-4329-9a5c-4c659567a652} MarkMpn.Sql4Cds.Engine.NetFx + + {920bac0a-847b-467b-adaa-674df59209f2} + MarkMpn.Sql4Cds.Export + @@ -309,4 +313,10 @@ copy $(TargetDir)MarkMpn.Sql4Cds.XTB.dll %25appdata%25\MscrmTools\XrmToolBox\Plugins\MarkMpn.Sql4Cds + + copy $(TargetDir)MarkMpn.Sql4Cds.XTB.dll %25appdata%25\MscrmTools\XrmToolBox\Plugins\MarkMpn.Sql4Cds +copy $(TargetDir)MarkMpn.Sql4Cds.Export.dll %25appdata%25\MscrmTools\XrmToolBox\Plugins\MarkMpn.Sql4Cds +copy $(TargetDir)SkiaSharp.dll %25appdata%25\MscrmTools\XrmToolBox\Plugins\MarkMpn.Sql4Cds +copy $(TargetDir)System.Text.Encoding.CodePages.dll %25appdata%25\MscrmTools\XrmToolBox\Plugins\MarkMpn.Sql4Cds + \ No newline at end of file diff --git a/MarkMpn.Sql4Cds.XTB/SqlQueryControl.Designer.cs b/MarkMpn.Sql4Cds.XTB/SqlQueryControl.Designer.cs index 8fb4f589..42398c8a 100644 --- a/MarkMpn.Sql4Cds.XTB/SqlQueryControl.Designer.cs +++ b/MarkMpn.Sql4Cds.XTB/SqlQueryControl.Designer.cs @@ -57,6 +57,9 @@ private void InitializeComponent() this.backgroundWorker = new System.ComponentModel.BackgroundWorker(); this.timer = new System.Windows.Forms.Timer(this.components); this.environmentHighlightLabel = new System.Windows.Forms.Label(); + this.toolStripMenuItem2 = new System.Windows.Forms.ToolStripSeparator(); + this.saveAsCSVToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.saveAsExcelToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); ((System.ComponentModel.ISupportInitialize)(this.splitContainer)).BeginInit(); this.splitContainer.Panel1.SuspendLayout(); this.splitContainer.Panel2.SuspendLayout(); @@ -178,7 +181,10 @@ private void InitializeComponent() this.toolStripMenuItem1, this.openRecordToolStripMenuItem, this.copyRecordUrlToolStripMenuItem, - this.createSELECTStatementToolStripMenuItem}); + this.createSELECTStatementToolStripMenuItem, + this.toolStripMenuItem2, + this.saveAsCSVToolStripMenuItem, + this.saveAsExcelToolStripMenuItem}); this.gridContextMenuStrip.Name = "gridContextMenuStrip"; this.gridContextMenuStrip.Size = new System.Drawing.Size(207, 142); this.gridContextMenuStrip.Opening += new System.ComponentModel.CancelEventHandler(this.gridContextMenuStrip_Opening); @@ -336,6 +342,25 @@ private void InitializeComponent() this.environmentHighlightLabel.Text = "Environment Name"; this.environmentHighlightLabel.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; // + // toolStripMenuItem2 + // + this.toolStripMenuItem2.Name = "toolStripMenuItem2"; + this.toolStripMenuItem2.Size = new System.Drawing.Size(203, 6); + // + // saveAsCSVToolStripMenuItem + // + this.saveAsCSVToolStripMenuItem.Name = "saveAsCSVToolStripMenuItem"; + this.saveAsCSVToolStripMenuItem.Size = new System.Drawing.Size(206, 22); + this.saveAsCSVToolStripMenuItem.Text = "Save As CSV..."; + this.saveAsCSVToolStripMenuItem.Click += new System.EventHandler(this.saveAsCSVToolStripMenuItem_Click); + // + // saveAsExcelToolStripMenuItem + // + this.saveAsExcelToolStripMenuItem.Name = "saveAsExcelToolStripMenuItem"; + this.saveAsExcelToolStripMenuItem.Size = new System.Drawing.Size(206, 22); + this.saveAsExcelToolStripMenuItem.Text = "Save As Excel..."; + this.saveAsExcelToolStripMenuItem.Click += new System.EventHandler(this.saveAsExcelToolStripMenuItem_Click); + // // SqlQueryControl // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); @@ -389,5 +414,8 @@ private void InitializeComponent() private System.Windows.Forms.ToolStripMenuItem createSELECTStatementToolStripMenuItem; private System.Windows.Forms.Label environmentHighlightLabel; private System.Windows.Forms.ToolStripMenuItem copyRecordUrlToolStripMenuItem; + private System.Windows.Forms.ToolStripSeparator toolStripMenuItem2; + private System.Windows.Forms.ToolStripMenuItem saveAsCSVToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem saveAsExcelToolStripMenuItem; } } diff --git a/MarkMpn.Sql4Cds.XTB/SqlQueryControl.cs b/MarkMpn.Sql4Cds.XTB/SqlQueryControl.cs index c53f4c93..dcfa1bc5 100644 --- a/MarkMpn.Sql4Cds.XTB/SqlQueryControl.cs +++ b/MarkMpn.Sql4Cds.XTB/SqlQueryControl.cs @@ -18,9 +18,12 @@ using MarkMpn.Sql4Cds.Controls; using MarkMpn.Sql4Cds.Engine; using MarkMpn.Sql4Cds.Engine.ExecutionPlan; +using MarkMpn.Sql4Cds.Export; using McTools.Xrm.Connection; using Microsoft.ApplicationInsights; using Microsoft.SqlServer.TransactSql.ScriptDom; +using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage; using Microsoft.Xrm.Sdk; using Microsoft.Xrm.Sdk.Query; using Microsoft.Xrm.Tooling.Connector; @@ -61,6 +64,47 @@ public TextRange(int index, int length) public int Length { get; } } + abstract class ExportParams + { + public DataTable DataTable { get; set; } + + public string Filename { get; set; } + + protected abstract IFileStreamFactory GetFileStreamFactory(); + + public void Export(Action progress) + { + var cols = new List(); + + for (var i = 0; i < DataTable.Columns.Count; i++) + { + var col = DataTable.Columns[i]; + var schema = (DataRow)col.ExtendedProperties["Schema"]; + var type = (string)schema["DataTypeName"]; + var scale = (short)schema["NumericScale"]; + cols.Add(new DbColumnWrapper(col.Caption, type, scale)); + } + + using (var stream = GetFileStreamFactory().GetWriter(Filename, cols)) + { + var cells = new DbCellValue[cols.Count]; + + for (var row = 0; row < DataTable.Rows.Count; row++) + { + for (var col = 0; col < cols.Count; col++) + cells[col] = ValueFormatter.Format(DataTable.Rows[row][col], cols[col].DataTypeName, cols[col].NumericScale, Settings.Instance.LocalFormatDates); + + stream.WriteRow(cells, cols); + + if ((row + 1) % 100 == 0) + progress(row + 1); + } + + progress(DataTable.Rows.Count); + } + } + } + private ConnectionDetail _con; private readonly TelemetryClient _ai; private readonly Scintilla _editor; @@ -85,6 +129,7 @@ public TextRange(int index, int length) private FindReplace _findReplace; private bool _ctrlK; private Font _linkFont; + private BackgroundWorker _exportBackgroundWorker; static SqlQueryControl() { @@ -97,6 +142,13 @@ static SqlQueryControl() public SqlQueryControl(ConnectionDetail con, IDictionary dataSources, TelemetryClient ai, Action showFetchXml, Action log, PropertiesWindow properties) { InitializeComponent(); + + _exportBackgroundWorker = new BackgroundWorker(); + _exportBackgroundWorker.DoWork += exportBackgroundWorker_DoWork; + _exportBackgroundWorker.ProgressChanged += backgroundWorker_ProgressChanged; + _exportBackgroundWorker.RunWorkerCompleted += exportBackgroundWorker_RunWorkerCompleted; + _exportBackgroundWorker.WorkerReportsProgress = true; + DisplayName = $"SQLQuery{++_queryCounter}.sql"; ShowFetchXML = showFetchXml; DataSources = dataSources; @@ -142,7 +194,7 @@ public override string Content public Action ShowFetchXML { get; } public ConnectionDetail Connection => _con; - + public string Sql => String.IsNullOrEmpty(_editor.SelectedText) ? _editor.Text : _editor.SelectedText; public override void SettingsChanged() @@ -576,7 +628,7 @@ public void Execute(bool execute, bool includeFetchXml) if (Connection == null) return; - if (backgroundWorker.IsBusy) + if (Busy) return; var offset = String.IsNullOrEmpty(_editor.SelectedText) ? 0 : _editor.SelectionStart; @@ -592,7 +644,7 @@ public void Execute(bool execute, bool includeFetchXml) backgroundWorker.RunWorkerAsync(_params); } - public bool Busy => backgroundWorker.IsBusy; + public bool Busy => backgroundWorker.IsBusy || _exportBackgroundWorker.IsBusy; public event EventHandler BusyChanged; @@ -1257,85 +1309,32 @@ private void ShowResult(IRootExecutionPlanNode query, ExecuteParams args, DataTa private void FormatCell(object sender, DataGridViewCellFormattingEventArgs e) { - var grid = (DataGridView)sender; - var results = (DataTable)grid.DataSource; + var results = sender as DataTable; - if (e.Value is DBNull || (e.Value is INullable nullable && nullable.IsNull)) - { - e.Value = "NULL"; + if (sender is DataGridView grid) + results = (DataTable)grid.DataSource; - if (e.CellStyle != null) - e.CellStyle.BackColor = Color.FromArgb(0xff, 0xff, 0xe1); - - e.FormattingApplied = true; - } - else if (e.Value is bool b) - { - e.Value = b ? "1" : "0"; - e.FormattingApplied = true; - } - else if (e.Value is DateTime dt) - { - var schema = (DataRow)results.Columns[e.ColumnIndex].ExtendedProperties["Schema"]; - var type = (string)schema["DataTypeName"]; - - if (type == "date") - { - if (Settings.Instance.LocalFormatDates) - e.Value = dt.ToShortDateString(); - else - e.Value = dt.ToString("yyyy-MM-dd"); + var col = results.Columns[e.ColumnIndex]; + var schema = (DataRow)col.ExtendedProperties["Schema"]; + var type = (string)schema["DataTypeName"]; + var scale = (short)schema["NumericScale"]; + var cell = ValueFormatter.Format(e.Value, type, scale, Settings.Instance.LocalFormatDates); - e.FormattingApplied = true; - } - else if (type == "smalldatetime") - { - if (Settings.Instance.LocalFormatDates) - e.Value = dt.ToShortDateString() + " " + dt.ToString("HH:mm"); - else - e.Value = dt.ToString("yyyy-MM-dd HH:mm"); + e.Value = cell.DisplayValue; + e.FormattingApplied = true; - e.FormattingApplied = true; - } - else if (!Settings.Instance.LocalFormatDates) - { - var scale = (short)schema["NumericScale"]; - e.Value = dt.ToString("yyyy-MM-dd HH:mm:ss" + (scale == 0 ? "" : ("." + new string('f', scale)))); - e.FormattingApplied = true; - } - } - else if (e.Value is TimeSpan ts && !Settings.Instance.LocalFormatDates) - { - var schema = (DataRow)results.Columns[e.ColumnIndex].ExtendedProperties["Schema"]; - var scale = (short)schema["NumericScale"]; - e.Value = ts.ToString("hh\\:mm\\:ss" + (scale == 0 ? "" : ("\\." + new string('f', scale)))); - e.FormattingApplied = true; - } - else if (e.Value is decimal dec) - { - var schema = (DataRow)results.Columns[e.ColumnIndex].ExtendedProperties["Schema"]; - var scale = (short)schema["NumericScale"]; - e.Value = dec.ToString("0" + (scale == 0 ? "" : ("." + new string('0', scale)))); - e.FormattingApplied = true; - } - else if (e.Value is SqlEntityReference) + if (cell.IsNull) { if (e.CellStyle != null) - { - e.CellStyle.ForeColor = SystemColors.HotTrack; - e.CellStyle.Font = _linkFont; - } + e.CellStyle.BackColor = Color.FromArgb(0xff, 0xff, 0xe1); } - else if (e.Value is SqlXml xml) + else if (e.Value is SqlEntityReference || e.Value is SqlXml) { if (e.CellStyle != null) { e.CellStyle.ForeColor = SystemColors.HotTrack; e.CellStyle.Font = _linkFont; } - - e.Value = xml.Value; - e.FormattingApplied = true; } } @@ -1377,7 +1376,7 @@ private void OpenRecord(SqlEntityReference entityReference) private void backgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e) { toolStripStatusLabel.Image = null; - toolStripStatusLabel.Text = (string) e.UserState; + toolStripStatusLabel.Text = (string)e.UserState; } private void timer_Tick(object sender, EventArgs e) @@ -1428,7 +1427,7 @@ private int GetMinHeight(Control control, int max) } if (control is Scintilla scintilla) - return (int) ((scintilla.Lines.Count + 1) * scintilla.Styles[Style.Default].Size * 1.6) + 20; + return (int)((scintilla.Lines.Count + 1) * scintilla.Styles[Style.Default].Size * 1.6) + 20; if (control is Panel panel) return panel.Controls.OfType().Sum(child => GetMinHeight(child, max)); @@ -1450,7 +1449,7 @@ private void SyncUsername() else { var service = (CrmServiceClient)DataSources[Connection.ConnectionName].Connection; - + if (service.CallerId == Guid.Empty) { usernameDropDownButton.Text = _con.UserName; @@ -1512,6 +1511,9 @@ private void gridContextMenuStrip_Opening(object sender, CancelEventArgs e) } copyToolStripMenuItem.Enabled = grid.Rows.Count > 0; + + saveAsCSVToolStripMenuItem.Enabled = !Busy; + saveAsExcelToolStripMenuItem.Enabled = !Busy; } private void openRecordToolStripMenuItem_Click(object sender, EventArgs e) @@ -1843,5 +1845,135 @@ public static Control GetFocusedControl(Control control) } return control; } + + private void saveAsCSVToolStripMenuItem_Click(object sender, EventArgs e) + { + var grid = (DataGridView)gridContextMenuStrip.SourceControl; + var table = (DataTable)grid.DataSource; + + using (var saveDialog = new SaveFileDialog()) + { + saveDialog.Filter = "CSV Files (*.csv)|*.csv"; + + if (saveDialog.ShowDialog() == DialogResult.OK) + { + _exportBackgroundWorker.RunWorkerAsync(new ExportToCsv(table, saveDialog.FileName)); + } + } + } + + class ExportToCsv : ExportParams + { + public ExportToCsv(DataTable table, string filename) + { + DataTable = table; + Filename = filename; + } + + protected override IFileStreamFactory GetFileStreamFactory() + { + return new SaveAsCsvFileStreamFactory + { + SaveRequestParams = new SaveResultsAsCsvRequestParams + { + FilePath = Filename, + IncludeHeaders = true + } + }; + } + } + + private void saveAsExcelToolStripMenuItem_Click(object sender, EventArgs e) + { + var grid = (DataGridView)gridContextMenuStrip.SourceControl; + var table = (DataTable)grid.DataSource; + + using (var saveDialog = new SaveFileDialog()) + { + saveDialog.Filter = "Excel Files (*.xlsx)|*.xlsx"; + + if (saveDialog.ShowDialog() == DialogResult.OK) + { + _exportBackgroundWorker.RunWorkerAsync(new ExportToExcel(table, saveDialog.FileName)); + } + } + } + + class ExportToExcel : ExportParams + { + public ExportToExcel(DataTable table, string filename) + { + DataTable = table; + Filename = filename; + } + + protected override IFileStreamFactory GetFileStreamFactory() + { + return new SaveAsExcelFileStreamFactory + { + SaveRequestParams = new SaveResultsAsExcelRequestParams + { + FilePath = Filename, + IncludeHeaders = true, + BoldHeaderRow = true + } + }; + } + } + + private void exportBackgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) + { + _progressHost.Visible = false; + _stopwatch.Stop(); + timer.Enabled = false; + + if (e.Error != null) + { + toolStripStatusLabel.Image = Properties.Resources.StatusWarning_16x; + toolStripStatusLabel.Text = "Export failed"; + MessageBox.Show(e.Error.Message, "Export Failed", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + else + { + toolStripStatusLabel.Image = Properties.Resources.StatusOK_16x; + toolStripStatusLabel.Text = "Export completed"; + } + + BusyChanged?.Invoke(this, EventArgs.Empty); + } + + private void exportBackgroundWorker_DoWork(object sender, DoWorkEventArgs e) + { + BusyChanged?.Invoke(this, EventArgs.Empty); + + var exportParams = (ExportParams)e.Argument; + + Execute(() => + { + _progressHost.Visible = true; + timerLabel.Text = "00:00:00"; + _stopwatch.Restart(); + timer.Enabled = true; + _rowCount = 0; + rowsLabel.Text = "0 rows"; + }); + + _exportBackgroundWorker.ReportProgress(0, "Exporting data..."); + + exportParams.Export(rows => + { + Execute(() => + { + _rowCount = rows; + + if (_rowCount == 1) + rowsLabel.Text = "1 row"; + else + rowsLabel.Text = $"{_rowCount:N0} rows"; + }); + }); + + e.Result = exportParams; + } } } diff --git a/MarkMpn.Sql4Cds.XTB/SqlQueryControl.resx b/MarkMpn.Sql4Cds.XTB/SqlQueryControl.resx index 4b4bd978..32d98a6a 100644 --- a/MarkMpn.Sql4Cds.XTB/SqlQueryControl.resx +++ b/MarkMpn.Sql4Cds.XTB/SqlQueryControl.resx @@ -125,7 +125,7 @@ AAEAAAD/////AQAAAAAAAAAMAgAAAFdTeXN0ZW0uV2luZG93cy5Gb3JtcywgVmVyc2lvbj00LjAuMC4w LCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODkFAQAAACZTeXN0 ZW0uV2luZG93cy5Gb3Jtcy5JbWFnZUxpc3RTdHJlYW1lcgEAAAAERGF0YQcCAgAAAAkDAAAADwMAAACM - CQAAAk1TRnQBSQFMAgEBAwEAAYABAAGAAQABEAEAARABAAT/AQkBAAj/AUIBTQE2AQQGAAE2AQQCAAEo + CQAAAk1TRnQBSQFMAgEBAwEAAYgBAAGIAQABEAEAARABAAT/AQkBAAj/AUIBTQE2AQQGAAE2AQQCAAEo AwABQAMAARADAAEBAQABCAYAAQQYAAGAAgABgAMAAoABAAGAAwABgAEAAYABAAKAAgADwAEAAcAB3AHA AQAB8AHKAaYBAAEzBQABMwEAATMBAAEzAQACMwIAAxYBAAMcAQADIgEAAykBAANVAQADTQEAA0IBAAM5 AQABgAF8Af8BAAJQAf8BAAGTAQAB1gEAAf8B7AHMAQABxgHWAe8BAAHWAucBAAGQAakBrQIAAf8BMwMA diff --git a/MarkMpn.Sql4Cds.sln b/MarkMpn.Sql4Cds.sln index 8ad7ddb9..c835fea2 100644 --- a/MarkMpn.Sql4Cds.sln +++ b/MarkMpn.Sql4Cds.sln @@ -57,6 +57,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MarkMpn.Sql4Cds.DebugVisual EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MarkMpn.Sql4Cds.DebugVisualizer.DebugeeSide", "MarkMpn.Sql4Cds.DebugVisualizer.DebugeeSide\MarkMpn.Sql4Cds.DebugVisualizer.DebugeeSide.csproj", "{65FD7411-5B94-443B-A4C3-A66082E0B0AE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MarkMpn.Sql4Cds.Export", "MarkMpn.Sql4Cds.Export\MarkMpn.Sql4Cds.Export.csproj", "{920BAC0A-847B-467B-ADAA-674DF59209F2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -265,6 +267,18 @@ Global {65FD7411-5B94-443B-A4C3-A66082E0B0AE}.Release|arm64.Build.0 = Release|Any CPU {65FD7411-5B94-443B-A4C3-A66082E0B0AE}.Release|x86.ActiveCfg = Release|Any CPU {65FD7411-5B94-443B-A4C3-A66082E0B0AE}.Release|x86.Build.0 = Release|Any CPU + {920BAC0A-847B-467B-ADAA-674DF59209F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {920BAC0A-847B-467B-ADAA-674DF59209F2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {920BAC0A-847B-467B-ADAA-674DF59209F2}.Debug|arm64.ActiveCfg = Debug|Any CPU + {920BAC0A-847B-467B-ADAA-674DF59209F2}.Debug|arm64.Build.0 = Debug|Any CPU + {920BAC0A-847B-467B-ADAA-674DF59209F2}.Debug|x86.ActiveCfg = Debug|Any CPU + {920BAC0A-847B-467B-ADAA-674DF59209F2}.Debug|x86.Build.0 = Debug|Any CPU + {920BAC0A-847B-467B-ADAA-674DF59209F2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {920BAC0A-847B-467B-ADAA-674DF59209F2}.Release|Any CPU.Build.0 = Release|Any CPU + {920BAC0A-847B-467B-ADAA-674DF59209F2}.Release|arm64.ActiveCfg = Release|Any CPU + {920BAC0A-847B-467B-ADAA-674DF59209F2}.Release|arm64.Build.0 = Release|Any CPU + {920BAC0A-847B-467B-ADAA-674DF59209F2}.Release|x86.ActiveCfg = Release|Any CPU + {920BAC0A-847B-467B-ADAA-674DF59209F2}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 50ade3c0e58a7587c06a22bd58ab844a34581c14 Mon Sep 17 00:00:00 2001 From: Mark Carrington <31017244+MarkMpn@users.noreply.github.com> Date: Sat, 29 Jun 2024 16:46:23 +0100 Subject: [PATCH 12/25] Refactored multi-targeting --- .../MarkMpn.Sql4Cds.Controls.csproj | 12 +- ...Sql4Cds.DebugVisualizer.DebugeeSide.csproj | 3 +- ...ql4Cds.DebugVisualizer.DebuggerSide.csproj | 1 - .../MarkMpn.Sql4Cds.Engine.NetCore.csproj | 20 --- .../MarkMpn.Sql4Cds.Engine.NetFx.csproj | 98 -------------- .../Properties/AssemblyInfo.cs | 39 ------ .../Properties/Resources.Designer.cs | 63 --------- .../Properties/Resources.resx | 120 ------------------ .../MarkMpn.Sql4Cds.Engine.Tests.csproj | 6 +- MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsError.cs | 2 +- MarkMpn.Sql4Cds.Engine/AssemblyInfo.cs | 3 + MarkMpn.Sql4Cds.Engine/Collation.cs | 2 +- .../Key.snk | Bin .../MarkMpn.Sql4Cds.Engine.csproj | 42 ++++++ .../MarkMpn.Sql4Cds.Engine.shproj | 13 -- .../MarkMpn.Sql4Cds.LanguageServer.csproj | 2 +- .../MarkMpn.Sql4Cds.SSMS.18.csproj | 6 +- .../MarkMpn.Sql4Cds.SSMS.19.csproj | 6 +- .../MarkMpn.Sql4Cds.SSMS.20.csproj | 6 +- .../MarkMpn.Sql4Cds.Tests.csproj | 6 +- .../MarkMpn.Sql4Cds.XTB.csproj | 6 +- MarkMpn.Sql4Cds.sln | 47 ++----- MarkMpn.Sql4Cds/MarkMpn.Sql4Cds.csproj | 6 +- 23 files changed, 90 insertions(+), 419 deletions(-) delete mode 100644 MarkMpn.Sql4Cds.Engine.NetCore/MarkMpn.Sql4Cds.Engine.NetCore.csproj delete mode 100644 MarkMpn.Sql4Cds.Engine.NetFx/MarkMpn.Sql4Cds.Engine.NetFx.csproj delete mode 100644 MarkMpn.Sql4Cds.Engine.NetFx/Properties/AssemblyInfo.cs delete mode 100644 MarkMpn.Sql4Cds.Engine.NetFx/Properties/Resources.Designer.cs delete mode 100644 MarkMpn.Sql4Cds.Engine.NetFx/Properties/Resources.resx create mode 100644 MarkMpn.Sql4Cds.Engine/AssemblyInfo.cs rename {MarkMpn.Sql4Cds.Engine.NetFx => MarkMpn.Sql4Cds.Engine}/Key.snk (100%) create mode 100644 MarkMpn.Sql4Cds.Engine/MarkMpn.Sql4Cds.Engine.csproj delete mode 100644 MarkMpn.Sql4Cds.Engine/MarkMpn.Sql4Cds.Engine.shproj diff --git a/MarkMpn.Sql4Cds.Controls/MarkMpn.Sql4Cds.Controls.csproj b/MarkMpn.Sql4Cds.Controls/MarkMpn.Sql4Cds.Controls.csproj index b2a7c1bf..b28f5128 100644 --- a/MarkMpn.Sql4Cds.Controls/MarkMpn.Sql4Cds.Controls.csproj +++ b/MarkMpn.Sql4Cds.Controls/MarkMpn.Sql4Cds.Controls.csproj @@ -59,12 +59,6 @@ - - - {23288bb2-0d6f-4329-9a5c-4c659567a652} - MarkMpn.Sql4Cds.Engine.NetFx - - @@ -170,6 +164,12 @@ + + + {c77b731d-e55c-4197-b96c-2b23eb9f56ef} + MarkMpn.Sql4Cds.Engine + + copy $(TargetDir)MarkMpn.Sql4Cds.Controls.dll %25appdata%25\MscrmTools\XrmToolBox\Plugins\MarkMpn.Sql4Cds diff --git a/MarkMpn.Sql4Cds.DebugVisualizer.DebugeeSide/MarkMpn.Sql4Cds.DebugVisualizer.DebugeeSide.csproj b/MarkMpn.Sql4Cds.DebugVisualizer.DebugeeSide/MarkMpn.Sql4Cds.DebugVisualizer.DebugeeSide.csproj index 77abd04b..efa560b0 100644 --- a/MarkMpn.Sql4Cds.DebugVisualizer.DebugeeSide/MarkMpn.Sql4Cds.DebugVisualizer.DebugeeSide.csproj +++ b/MarkMpn.Sql4Cds.DebugVisualizer.DebugeeSide/MarkMpn.Sql4Cds.DebugVisualizer.DebugeeSide.csproj @@ -12,8 +12,7 @@ - - + diff --git a/MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide/MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide.csproj b/MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide/MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide.csproj index decc789f..eed07fee 100644 --- a/MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide/MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide.csproj +++ b/MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide/MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide.csproj @@ -43,6 +43,5 @@ - diff --git a/MarkMpn.Sql4Cds.Engine.NetCore/MarkMpn.Sql4Cds.Engine.NetCore.csproj b/MarkMpn.Sql4Cds.Engine.NetCore/MarkMpn.Sql4Cds.Engine.NetCore.csproj deleted file mode 100644 index 98cc8f71..00000000 --- a/MarkMpn.Sql4Cds.Engine.NetCore/MarkMpn.Sql4Cds.Engine.NetCore.csproj +++ /dev/null @@ -1,20 +0,0 @@ - - - - net6.0 - MarkMpn.Sql4Cds.Engine - MarkMpn.Sql4Cds.Engine - Copyright © 2020 - 2024 Mark Carrington - - - - - - - - - - - - - diff --git a/MarkMpn.Sql4Cds.Engine.NetFx/MarkMpn.Sql4Cds.Engine.NetFx.csproj b/MarkMpn.Sql4Cds.Engine.NetFx/MarkMpn.Sql4Cds.Engine.NetFx.csproj deleted file mode 100644 index e0298a70..00000000 --- a/MarkMpn.Sql4Cds.Engine.NetFx/MarkMpn.Sql4Cds.Engine.NetFx.csproj +++ /dev/null @@ -1,98 +0,0 @@ - - - - - Debug - AnyCPU - {23288BB2-0D6F-4329-9A5C-4C659567A652} - Library - Properties - MarkMpn.Sql4Cds.Engine - MarkMpn.Sql4Cds.Engine - v4.6.2 - 512 - true - - win - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - true - - - Key.snk - - - - - - - - - - - - - - - - - - - - - - - - - - 2.21.0 - - - 9.0.2.49 - - - 9.1.1.32 - - - 1.1.1 - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - 161.8834.0 - - - 1.1.3 - - - - - - - - - copy $(TargetDir)MarkMpn.Sql4Cds.Engine.dll %25appdata%25\MscrmTools\XrmToolBox\Plugins\MarkMpn.Sql4Cds -copy $(TargetDir)Microsoft.ApplicationInsights.dll %25appdata%25\MscrmTools\XrmToolBox\Plugins\MarkMpn.Sql4Cds -copy $(TargetDir)Microsoft.SqlServer.TransactSql.ScriptDom.dll %25appdata%25\MscrmTools\XrmToolBox\Plugins\MarkMpn.Sql4Cds -copy $(TargetDir)XPath2.dll %25appdata%25\MscrmTools\XrmToolBox\Plugins\MarkMpn.Sql4Cds -copy $(TargetDir)XPath2.Extensions.dll %25appdata%25\MscrmTools\XrmToolBox\Plugins\MarkMpn.Sql4Cds - - - \ No newline at end of file diff --git a/MarkMpn.Sql4Cds.Engine.NetFx/Properties/AssemblyInfo.cs b/MarkMpn.Sql4Cds.Engine.NetFx/Properties/AssemblyInfo.cs deleted file mode 100644 index 4245cbf0..00000000 --- a/MarkMpn.Sql4Cds.Engine.NetFx/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("MarkMpn.Sql4Cds.Engine")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Mark Carrington")] -[assembly: AssemblyProduct("MarkMpn.Sql4Cds.Engine")] -[assembly: AssemblyCopyright("Copyright © 2020 - 2024 Mark Carrington")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("583628f7-a027-451b-b3a1-e85ffb241dfb")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] -[assembly: AssemblyInformationalVersion("1.0.0.0")] - -[assembly: InternalsVisibleTo("MarkMpn.Sql4Cds.Engine.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c13706e77bd5be4fad5e04b334cb05a1a8d7fb12baea9b44579760cd26dec6e6d0f5496a2f7933cf44a172a0dc2bccbd94f090bc7f8b79fd76e244840f8447389d6d6bcbab1cf9085d1043140346ecd9f954a523a82861c596214ae0b92537d6dc6796ee649239684d66e45aada225102503a9f8c4034ab8e0a8bbadf9d210a8")] \ No newline at end of file diff --git a/MarkMpn.Sql4Cds.Engine.NetFx/Properties/Resources.Designer.cs b/MarkMpn.Sql4Cds.Engine.NetFx/Properties/Resources.Designer.cs deleted file mode 100644 index 181669fa..00000000 --- a/MarkMpn.Sql4Cds.Engine.NetFx/Properties/Resources.Designer.cs +++ /dev/null @@ -1,63 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace MarkMpn.Sql4Cds.Engine.Properties { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MarkMpn.Sql4Cds.Engine.Properties.Resources", typeof(Resources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - } -} diff --git a/MarkMpn.Sql4Cds.Engine.NetFx/Properties/Resources.resx b/MarkMpn.Sql4Cds.Engine.NetFx/Properties/Resources.resx deleted file mode 100644 index 1af7de15..00000000 --- a/MarkMpn.Sql4Cds.Engine.NetFx/Properties/Resources.resx +++ /dev/null @@ -1,120 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - \ No newline at end of file diff --git a/MarkMpn.Sql4Cds.Engine.Tests/MarkMpn.Sql4Cds.Engine.Tests.csproj b/MarkMpn.Sql4Cds.Engine.Tests/MarkMpn.Sql4Cds.Engine.Tests.csproj index 2b7dcf86..d502b328 100644 --- a/MarkMpn.Sql4Cds.Engine.Tests/MarkMpn.Sql4Cds.Engine.Tests.csproj +++ b/MarkMpn.Sql4Cds.Engine.Tests/MarkMpn.Sql4Cds.Engine.Tests.csproj @@ -111,9 +111,9 @@ - - {23288bb2-0d6f-4329-9a5c-4c659567a652} - MarkMpn.Sql4Cds.Engine.NetFx + + {c77b731d-e55c-4197-b96c-2b23eb9f56ef} + MarkMpn.Sql4Cds.Engine {a7af5d13-a44e-426d-b3fc-ae390832c7df} diff --git a/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsError.cs b/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsError.cs index 529f59e3..629d1639 100644 --- a/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsError.cs +++ b/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsError.cs @@ -24,7 +24,7 @@ static Sql4CdsError() { _errorNumberToDetails = new Dictionary(); - using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("MarkMpn.Sql4Cds.Engine.resources.Errors.csv")) + using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("MarkMpn.Sql4Cds.Engine.Resources.Errors.csv")) using (var reader = new StreamReader(stream)) { string line; diff --git a/MarkMpn.Sql4Cds.Engine/AssemblyInfo.cs b/MarkMpn.Sql4Cds.Engine/AssemblyInfo.cs new file mode 100644 index 00000000..ea6c5af7 --- /dev/null +++ b/MarkMpn.Sql4Cds.Engine/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("MarkMpn.Sql4Cds.Engine.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c13706e77bd5be4fad5e04b334cb05a1a8d7fb12baea9b44579760cd26dec6e6d0f5496a2f7933cf44a172a0dc2bccbd94f090bc7f8b79fd76e244840f8447389d6d6bcbab1cf9085d1043140346ecd9f954a523a82861c596214ae0b92537d6dc6796ee649239684d66e45aada225102503a9f8c4034ab8e0a8bbadf9d210a8")] \ No newline at end of file diff --git a/MarkMpn.Sql4Cds.Engine/Collation.cs b/MarkMpn.Sql4Cds.Engine/Collation.cs index 17949854..03974f01 100644 --- a/MarkMpn.Sql4Cds.Engine/Collation.cs +++ b/MarkMpn.Sql4Cds.Engine/Collation.cs @@ -19,7 +19,7 @@ static Collation() { _collationNameToLcid = new Dictionary(StringComparer.OrdinalIgnoreCase); - using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("MarkMpn.Sql4Cds.Engine.resources.CollationNameToLCID.txt")) + using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("MarkMpn.Sql4Cds.Engine.Resources.CollationNameToLCID.txt")) using (var reader = new StreamReader(stream)) { string line; diff --git a/MarkMpn.Sql4Cds.Engine.NetFx/Key.snk b/MarkMpn.Sql4Cds.Engine/Key.snk similarity index 100% rename from MarkMpn.Sql4Cds.Engine.NetFx/Key.snk rename to MarkMpn.Sql4Cds.Engine/Key.snk diff --git a/MarkMpn.Sql4Cds.Engine/MarkMpn.Sql4Cds.Engine.csproj b/MarkMpn.Sql4Cds.Engine/MarkMpn.Sql4Cds.Engine.csproj new file mode 100644 index 00000000..021569bc --- /dev/null +++ b/MarkMpn.Sql4Cds.Engine/MarkMpn.Sql4Cds.Engine.csproj @@ -0,0 +1,42 @@ + + + + net6.0;net462 + MarkMpn.Sql4Cds.Engine + MarkMpn.Sql4Cds.Engine + Copyright © 2020 - 2024 Mark Carrington + True + Key.snk + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MarkMpn.Sql4Cds.Engine/MarkMpn.Sql4Cds.Engine.shproj b/MarkMpn.Sql4Cds.Engine/MarkMpn.Sql4Cds.Engine.shproj deleted file mode 100644 index 9bfa37ea..00000000 --- a/MarkMpn.Sql4Cds.Engine/MarkMpn.Sql4Cds.Engine.shproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - bcf89341-e1a6-4106-9c12-1e15c9e7c58b - 14.0 - - - - - - - - diff --git a/MarkMpn.Sql4Cds.LanguageServer/MarkMpn.Sql4Cds.LanguageServer.csproj b/MarkMpn.Sql4Cds.LanguageServer/MarkMpn.Sql4Cds.LanguageServer.csproj index d2292c7a..b434010a 100644 --- a/MarkMpn.Sql4Cds.LanguageServer/MarkMpn.Sql4Cds.LanguageServer.csproj +++ b/MarkMpn.Sql4Cds.LanguageServer/MarkMpn.Sql4Cds.LanguageServer.csproj @@ -16,7 +16,7 @@ - + diff --git a/MarkMpn.Sql4Cds.SSMS.18/MarkMpn.Sql4Cds.SSMS.18.csproj b/MarkMpn.Sql4Cds.SSMS.18/MarkMpn.Sql4Cds.SSMS.18.csproj index 088020d6..a8b9dd56 100644 --- a/MarkMpn.Sql4Cds.SSMS.18/MarkMpn.Sql4Cds.SSMS.18.csproj +++ b/MarkMpn.Sql4Cds.SSMS.18/MarkMpn.Sql4Cds.SSMS.18.csproj @@ -162,9 +162,9 @@ {04c2d073-de54-4628-b876-5965d0b75b6e} MarkMpn.Sql4Cds.Controls - - {23288bb2-0d6f-4329-9a5c-4c659567a652} - MarkMpn.Sql4Cds.Engine.NetFx + + {c77b731d-e55c-4197-b96c-2b23eb9f56ef} + MarkMpn.Sql4Cds.Engine diff --git a/MarkMpn.Sql4Cds.SSMS.19/MarkMpn.Sql4Cds.SSMS.19.csproj b/MarkMpn.Sql4Cds.SSMS.19/MarkMpn.Sql4Cds.SSMS.19.csproj index 26f60fad..b5805a12 100644 --- a/MarkMpn.Sql4Cds.SSMS.19/MarkMpn.Sql4Cds.SSMS.19.csproj +++ b/MarkMpn.Sql4Cds.SSMS.19/MarkMpn.Sql4Cds.SSMS.19.csproj @@ -155,9 +155,9 @@ {04c2d073-de54-4628-b876-5965d0b75b6e} MarkMpn.Sql4Cds.Controls - - {23288bb2-0d6f-4329-9a5c-4c659567a652} - MarkMpn.Sql4Cds.Engine.NetFx + + {c77b731d-e55c-4197-b96c-2b23eb9f56ef} + MarkMpn.Sql4Cds.Engine diff --git a/MarkMpn.Sql4Cds.SSMS.20/MarkMpn.Sql4Cds.SSMS.20.csproj b/MarkMpn.Sql4Cds.SSMS.20/MarkMpn.Sql4Cds.SSMS.20.csproj index 1e4bde3e..2ed7bc0e 100644 --- a/MarkMpn.Sql4Cds.SSMS.20/MarkMpn.Sql4Cds.SSMS.20.csproj +++ b/MarkMpn.Sql4Cds.SSMS.20/MarkMpn.Sql4Cds.SSMS.20.csproj @@ -155,9 +155,9 @@ {04c2d073-de54-4628-b876-5965d0b75b6e} MarkMpn.Sql4Cds.Controls - - {23288bb2-0d6f-4329-9a5c-4c659567a652} - MarkMpn.Sql4Cds.Engine.NetFx + + {c77b731d-e55c-4197-b96c-2b23eb9f56ef} + MarkMpn.Sql4Cds.Engine diff --git a/MarkMpn.Sql4Cds.Tests/MarkMpn.Sql4Cds.Tests.csproj b/MarkMpn.Sql4Cds.Tests/MarkMpn.Sql4Cds.Tests.csproj index 28141e58..c04eae0b 100644 --- a/MarkMpn.Sql4Cds.Tests/MarkMpn.Sql4Cds.Tests.csproj +++ b/MarkMpn.Sql4Cds.Tests/MarkMpn.Sql4Cds.Tests.csproj @@ -82,9 +82,9 @@ - - {23288bb2-0d6f-4329-9a5c-4c659567a652} - MarkMpn.Sql4Cds.Engine.NetFx + + {c77b731d-e55c-4197-b96c-2b23eb9f56ef} + MarkMpn.Sql4Cds.Engine {8050b824-a28b-4631-8a95-d127859a9216} diff --git a/MarkMpn.Sql4Cds.XTB/MarkMpn.Sql4Cds.XTB.csproj b/MarkMpn.Sql4Cds.XTB/MarkMpn.Sql4Cds.XTB.csproj index eba275f6..d989849e 100644 --- a/MarkMpn.Sql4Cds.XTB/MarkMpn.Sql4Cds.XTB.csproj +++ b/MarkMpn.Sql4Cds.XTB/MarkMpn.Sql4Cds.XTB.csproj @@ -291,9 +291,9 @@ {04c2d073-de54-4628-b876-5965d0b75b6e} MarkMpn.Sql4Cds.Controls - - {23288bb2-0d6f-4329-9a5c-4c659567a652} - MarkMpn.Sql4Cds.Engine.NetFx + + {c77b731d-e55c-4197-b96c-2b23eb9f56ef} + MarkMpn.Sql4Cds.Engine {920bac0a-847b-467b-adaa-674df59209f2} diff --git a/MarkMpn.Sql4Cds.sln b/MarkMpn.Sql4Cds.sln index c835fea2..ca51da14 100644 --- a/MarkMpn.Sql4Cds.sln +++ b/MarkMpn.Sql4Cds.sln @@ -14,12 +14,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MarkMpn.Sql4Cds.Tests", "Ma EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MarkMpn.Sql4Cds.Engine.FetchXml.Tests", "MarkMpn.Sql4Cds.Engine.FetchXml.Tests\MarkMpn.Sql4Cds.Engine.FetchXml.Tests.csproj", "{69F7D111-3542-4E31-A0F8-E544F9FFA587}" EndProject -Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "MarkMpn.Sql4Cds.Engine", "MarkMpn.Sql4Cds.Engine\MarkMpn.Sql4Cds.Engine.shproj", "{BCF89341-E1A6-4106-9C12-1E15C9E7C58B}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MarkMpn.Sql4Cds.Engine.NetFx", "MarkMpn.Sql4Cds.Engine.NetFx\MarkMpn.Sql4Cds.Engine.NetFx.csproj", "{23288BB2-0D6F-4329-9A5C-4C659567A652}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MarkMpn.Sql4Cds.Engine.NetCore", "MarkMpn.Sql4Cds.Engine.NetCore\MarkMpn.Sql4Cds.Engine.NetCore.csproj", "{A570CBCA-D09C-40D5-A318-A95C8FBBD593}" -EndProject Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "MarkMpn.Sql4Cds.Engine.FetchXml", "MarkMpn.Sql4Cds.Engine\MarkMpn.Sql4Cds.Engine.FetchXml.shproj", "{460A4174-B9EA-413A-AF83-C27C0686E98F}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MarkMpn.Sql4Cds.Controls", "MarkMpn.Sql4Cds.Controls\MarkMpn.Sql4Cds.Controls.csproj", "{04C2D073-DE54-4628-B876-5965D0B75B6E}" @@ -59,6 +53,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MarkMpn.Sql4Cds.DebugVisual EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MarkMpn.Sql4Cds.Export", "MarkMpn.Sql4Cds.Export\MarkMpn.Sql4Cds.Export.csproj", "{920BAC0A-847B-467B-ADAA-674DF59209F2}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MarkMpn.Sql4Cds.Engine", "MarkMpn.Sql4Cds.Engine\MarkMpn.Sql4Cds.Engine.csproj", "{C77B731D-E55C-4197-B96C-2B23EB9F56EF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -117,30 +113,6 @@ Global {69F7D111-3542-4E31-A0F8-E544F9FFA587}.Release|arm64.Build.0 = Release|Any CPU {69F7D111-3542-4E31-A0F8-E544F9FFA587}.Release|x86.ActiveCfg = Release|Any CPU {69F7D111-3542-4E31-A0F8-E544F9FFA587}.Release|x86.Build.0 = Release|Any CPU - {23288BB2-0D6F-4329-9A5C-4C659567A652}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {23288BB2-0D6F-4329-9A5C-4C659567A652}.Debug|Any CPU.Build.0 = Debug|Any CPU - {23288BB2-0D6F-4329-9A5C-4C659567A652}.Debug|arm64.ActiveCfg = Debug|Any CPU - {23288BB2-0D6F-4329-9A5C-4C659567A652}.Debug|arm64.Build.0 = Debug|Any CPU - {23288BB2-0D6F-4329-9A5C-4C659567A652}.Debug|x86.ActiveCfg = Debug|Any CPU - {23288BB2-0D6F-4329-9A5C-4C659567A652}.Debug|x86.Build.0 = Debug|Any CPU - {23288BB2-0D6F-4329-9A5C-4C659567A652}.Release|Any CPU.ActiveCfg = Release|Any CPU - {23288BB2-0D6F-4329-9A5C-4C659567A652}.Release|Any CPU.Build.0 = Release|Any CPU - {23288BB2-0D6F-4329-9A5C-4C659567A652}.Release|arm64.ActiveCfg = Release|Any CPU - {23288BB2-0D6F-4329-9A5C-4C659567A652}.Release|arm64.Build.0 = Release|Any CPU - {23288BB2-0D6F-4329-9A5C-4C659567A652}.Release|x86.ActiveCfg = Release|Any CPU - {23288BB2-0D6F-4329-9A5C-4C659567A652}.Release|x86.Build.0 = Release|Any CPU - {A570CBCA-D09C-40D5-A318-A95C8FBBD593}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A570CBCA-D09C-40D5-A318-A95C8FBBD593}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A570CBCA-D09C-40D5-A318-A95C8FBBD593}.Debug|arm64.ActiveCfg = Debug|Any CPU - {A570CBCA-D09C-40D5-A318-A95C8FBBD593}.Debug|arm64.Build.0 = Debug|Any CPU - {A570CBCA-D09C-40D5-A318-A95C8FBBD593}.Debug|x86.ActiveCfg = Debug|Any CPU - {A570CBCA-D09C-40D5-A318-A95C8FBBD593}.Debug|x86.Build.0 = Debug|Any CPU - {A570CBCA-D09C-40D5-A318-A95C8FBBD593}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A570CBCA-D09C-40D5-A318-A95C8FBBD593}.Release|Any CPU.Build.0 = Release|Any CPU - {A570CBCA-D09C-40D5-A318-A95C8FBBD593}.Release|arm64.ActiveCfg = Release|Any CPU - {A570CBCA-D09C-40D5-A318-A95C8FBBD593}.Release|arm64.Build.0 = Release|Any CPU - {A570CBCA-D09C-40D5-A318-A95C8FBBD593}.Release|x86.ActiveCfg = Release|Any CPU - {A570CBCA-D09C-40D5-A318-A95C8FBBD593}.Release|x86.Build.0 = Release|Any CPU {04C2D073-DE54-4628-B876-5965D0B75B6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {04C2D073-DE54-4628-B876-5965D0B75B6E}.Debug|Any CPU.Build.0 = Debug|Any CPU {04C2D073-DE54-4628-B876-5965D0B75B6E}.Debug|arm64.ActiveCfg = Debug|Any CPU @@ -279,6 +251,18 @@ Global {920BAC0A-847B-467B-ADAA-674DF59209F2}.Release|arm64.Build.0 = Release|Any CPU {920BAC0A-847B-467B-ADAA-674DF59209F2}.Release|x86.ActiveCfg = Release|Any CPU {920BAC0A-847B-467B-ADAA-674DF59209F2}.Release|x86.Build.0 = Release|Any CPU + {C77B731D-E55C-4197-B96C-2B23EB9F56EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C77B731D-E55C-4197-B96C-2B23EB9F56EF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C77B731D-E55C-4197-B96C-2B23EB9F56EF}.Debug|arm64.ActiveCfg = Debug|Any CPU + {C77B731D-E55C-4197-B96C-2B23EB9F56EF}.Debug|arm64.Build.0 = Debug|Any CPU + {C77B731D-E55C-4197-B96C-2B23EB9F56EF}.Debug|x86.ActiveCfg = Debug|Any CPU + {C77B731D-E55C-4197-B96C-2B23EB9F56EF}.Debug|x86.Build.0 = Debug|Any CPU + {C77B731D-E55C-4197-B96C-2B23EB9F56EF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C77B731D-E55C-4197-B96C-2B23EB9F56EF}.Release|Any CPU.Build.0 = Release|Any CPU + {C77B731D-E55C-4197-B96C-2B23EB9F56EF}.Release|arm64.ActiveCfg = Release|Any CPU + {C77B731D-E55C-4197-B96C-2B23EB9F56EF}.Release|arm64.Build.0 = Release|Any CPU + {C77B731D-E55C-4197-B96C-2B23EB9F56EF}.Release|x86.ActiveCfg = Release|Any CPU + {C77B731D-E55C-4197-B96C-2B23EB9F56EF}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -291,14 +275,11 @@ Global SolutionGuid = {5E960561-7FE2-4022-964B-57E990767108} EndGlobalSection GlobalSection(SharedMSBuildProjectFiles) = preSolution - MarkMpn.Sql4Cds.Engine\MarkMpn.Sql4Cds.Engine.projitems*{23288bb2-0d6f-4329-9a5c-4c659567a652}*SharedItemsImports = 4 MarkMpn.Sql4Cds.SSMS\MarkMpn.Sql4Cds.SSMS.projitems*{2cfff3c0-5a6f-493b-86d7-4cc42b322393}*SharedItemsImports = 4 MarkMpn.Sql4Cds.Engine\MarkMpn.Sql4Cds.Engine.FetchXml.projitems*{460a4174-b9ea-413a-af83-c27c0686e98f}*SharedItemsImports = 13 MarkMpn.Sql4Cds.Engine\MarkMpn.Sql4Cds.Engine.FetchXml.projitems*{69f7d111-3542-4e31-a0f8-e544f9ffa587}*SharedItemsImports = 4 MarkMpn.Sql4Cds.SSMS\MarkMpn.Sql4Cds.SSMS.projitems*{73283a07-671d-4ba2-b922-98987f85330b}*SharedItemsImports = 13 MarkMpn.Sql4Cds.SSMS\MarkMpn.Sql4Cds.SSMS.projitems*{8c71690c-4ef8-4ebf-88c0-33952fed3acf}*SharedItemsImports = 4 - MarkMpn.Sql4Cds.Engine\MarkMpn.Sql4Cds.Engine.projitems*{a570cbca-d09c-40d5-a318-a95c8fbbd593}*SharedItemsImports = 5 MarkMpn.Sql4Cds.SSMS\MarkMpn.Sql4Cds.SSMS.projitems*{baf95ec5-addf-422a-a40c-cb8f5da1c445}*SharedItemsImports = 4 - MarkMpn.Sql4Cds.Engine\MarkMpn.Sql4Cds.Engine.projitems*{bcf89341-e1a6-4106-9c12-1e15c9e7c58b}*SharedItemsImports = 13 EndGlobalSection EndGlobal diff --git a/MarkMpn.Sql4Cds/MarkMpn.Sql4Cds.csproj b/MarkMpn.Sql4Cds/MarkMpn.Sql4Cds.csproj index 14525b16..cbc4180c 100644 --- a/MarkMpn.Sql4Cds/MarkMpn.Sql4Cds.csproj +++ b/MarkMpn.Sql4Cds/MarkMpn.Sql4Cds.csproj @@ -80,9 +80,9 @@ {04c2d073-de54-4628-b876-5965d0b75b6e} MarkMpn.Sql4Cds.Controls - - {23288bb2-0d6f-4329-9a5c-4c659567a652} - MarkMpn.Sql4Cds.Engine.NetFx + + {c77b731d-e55c-4197-b96c-2b23eb9f56ef} + MarkMpn.Sql4Cds.Engine From d2b663f45ec671fafd0208c6db4c3a9f81215a1d Mon Sep 17 00:00:00 2001 From: Mark Carrington <31017244+MarkMpn@users.noreply.github.com> Date: Sun, 30 Jun 2024 17:18:36 +0100 Subject: [PATCH 13/25] Made entity references clickable links in Excel exports --- .../DataStorage/IFileStreamWriter.cs | 1 + .../SaveAsExcelFileStreamFactory.cs | 9 +- .../SaveAsExcelFileStreamWriter.cs | 7 +- .../SaveAsExcelFileStreamWriterHelper.cs | 83 +++++++++++++++++-- .../MarkMpn.Sql4Cds.Export.csproj | 4 + MarkMpn.Sql4Cds.XTB/SqlQueryControl.cs | 12 ++- 6 files changed, 104 insertions(+), 12 deletions(-) diff --git a/MarkMpn.Sql4Cds.Export/DataStorage/IFileStreamWriter.cs b/MarkMpn.Sql4Cds.Export/DataStorage/IFileStreamWriter.cs index 51399534..dd5b0311 100644 --- a/MarkMpn.Sql4Cds.Export/DataStorage/IFileStreamWriter.cs +++ b/MarkMpn.Sql4Cds.Export/DataStorage/IFileStreamWriter.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; +using MarkMpn.Sql4Cds.Engine; using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; diff --git a/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsExcelFileStreamFactory.cs b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsExcelFileStreamFactory.cs index 6184de15..961769f9 100644 --- a/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsExcelFileStreamFactory.cs +++ b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsExcelFileStreamFactory.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.IO; +using MarkMpn.Sql4Cds.Engine; using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; using Microsoft.SqlTools.ServiceLayer.Utility; @@ -24,6 +25,11 @@ public class SaveAsExcelFileStreamFactory : IFileStreamFactory ///
public SaveResultsAsExcelRequestParams SaveRequestParams { get; set; } + /// + /// A function to create a URL from a SqlEntityReference + /// + public Func UrlGenerator { get; set; } + #endregion /// @@ -62,7 +68,8 @@ public IFileStreamWriter GetWriter(string fileName, IReadOnlyList urlGenerator; private bool headerWritten; private SaveAsExcelFileStreamWriterHelper.ExcelSheet sheet; @@ -50,11 +52,12 @@ public class SaveAsExcelFileStreamWriter : SaveAsStreamWriter /// The entire list of columns for the result set. They will be filtered down as per the /// request params. /// - public SaveAsExcelFileStreamWriter(Stream stream, SaveResultsAsExcelRequestParams requestParams, IReadOnlyList columns) + public SaveAsExcelFileStreamWriter(Stream stream, SaveResultsAsExcelRequestParams requestParams, IReadOnlyList columns, Func urlGenerator) : base(stream, requestParams, columns) { saveParams = requestParams; helper = new SaveAsExcelFileStreamWriterHelper(stream); + this.urlGenerator = urlGenerator; // Do some setup if the caller requested automatically sized columns if (requestParams.AutoSizeColumns) @@ -198,7 +201,7 @@ public override void WriteRow(IList row, IReadOnlyList } + /// + /// Write an entity reference cell + /// + /// Entity reference value to write + public void AddCell(SqlEntityReference value, Func urlGenerator) + { + // string needs string + // This class uses inlineStr instead of more common shared string table + // to improve write performance and reduce implementation complexity + referenceManager.AssureColumnReference(); + if (value.IsNull) + { + AddCellEmpty(); + return; + } + + writer.WriteStartElement("c"); + + referenceManager.WriteAndIncreaseColumnReference(); + + writer.WriteAttributeString("t", "inlineStr"); + writer.WriteAttributeString("s", "6"); + + writer.WriteStartElement("f"); + writer.WriteValue($"HYPERLINK(\"{urlGenerator(value)}\",\"{value.Id}\")"); + writer.WriteEndElement(); // + writer.WriteStartElement("v"); + writer.WriteValue(value.Id.ToString()); + writer.WriteEndElement(); // + + writer.WriteEndElement(); // + } + /// /// Write a object cell /// /// The program will try to output number/datetime, otherwise, call the ToString /// DbCellValue to write based on data type /// Whether the cell should be bold, defaults to false - public void AddCell(DbCellValue dbCellValue, bool bold = false) + public void AddCell(DbCellValue dbCellValue, bool bold = false, Func urlGenerator = null) { object o = dbCellValue.RawObject; if (dbCellValue.IsNull || o == null) @@ -207,6 +241,10 @@ public void AddCell(DbCellValue dbCellValue, bool bold = false) { AddCellBoxedNumber(dbCellValue.DisplayValue); } + else if (o is SqlEntityReference er && urlGenerator != null) + { + AddCell(er, urlGenerator); + } else { AddCell(dbCellValue.DisplayValue, bold); @@ -847,7 +885,7 @@ private void WriteStyle() xw.WriteStartElement("fonts"); - xw.WriteAttributeString("count", "2"); + xw.WriteAttributeString("count", "3"); xw.WriteStartElement("font"); xw.WriteStartElement("sz"); @@ -887,6 +925,26 @@ private void WriteStyle() xw.WriteEndElement(); // xw.WriteEndElement(); // + xw.WriteStartElement("font"); + xw.WriteStartElement("u"); + xw.WriteEndElement(); // + xw.WriteStartElement("sz"); + xw.WriteAttributeString("val", "11"); + xw.WriteEndElement(); // + xw.WriteStartElement("color"); + xw.WriteAttributeString("theme", "10"); + xw.WriteEndElement(); // + xw.WriteStartElement("name"); + xw.WriteAttributeString("val", "Calibri"); + xw.WriteEndElement(); // + xw.WriteStartElement("family"); + xw.WriteAttributeString("val", "2"); + xw.WriteEndElement(); // + xw.WriteStartElement("scheme"); + xw.WriteAttributeString("val", "minor"); + xw.WriteEndElement(); // + xw.WriteEndElement(); // + xw.WriteEndElement(); // fonts xw.WriteStartElement("fills"); @@ -910,17 +968,23 @@ private void WriteStyle() xw.WriteEndElement(); // xw.WriteStartElement("cellStyleXfs"); - xw.WriteAttributeString("count", "1"); + xw.WriteAttributeString("count", "2"); xw.WriteStartElement("xf"); xw.WriteAttributeString("numFmtId", "0"); xw.WriteAttributeString("fontId", "0"); xw.WriteAttributeString("fillId", "0"); xw.WriteAttributeString("borderId", "0"); xw.WriteEndElement(); // + xw.WriteStartElement("xf"); + xw.WriteAttributeString("numFmtId", "0"); + xw.WriteAttributeString("fontId", "2"); + xw.WriteAttributeString("fillId", "0"); + xw.WriteAttributeString("borderId", "0"); + xw.WriteEndElement(); // xw.WriteEndElement(); // xw.WriteStartElement("cellXfs"); - xw.WriteAttributeString("count", "6"); + xw.WriteAttributeString("count", "7"); xw.WriteStartElement("xf"); xw.WriteAttributeString("xfId", "0"); xw.WriteEndElement(); // @@ -948,10 +1012,19 @@ private void WriteStyle() xw.WriteAttributeString("xfId", "0"); xw.WriteAttributeString("fontId", "1"); xw.WriteEndElement(); // + xw.WriteStartElement("xf"); + xw.WriteAttributeString("xfId", "1"); + xw.WriteAttributeString("fontId", "2"); + xw.WriteEndElement(); // xw.WriteEndElement(); // xw.WriteStartElement("cellStyles"); - xw.WriteAttributeString("count", "1"); + xw.WriteAttributeString("count", "2"); + xw.WriteStartElement("cellStyle"); + xw.WriteAttributeString("name", "Hyperlink"); + xw.WriteAttributeString("builtinId", "8"); + xw.WriteAttributeString("xfId", "1"); + xw.WriteEndElement(); // xw.WriteStartElement("cellStyle"); xw.WriteAttributeString("name", "Normal"); xw.WriteAttributeString("builtinId", "0"); diff --git a/MarkMpn.Sql4Cds.Export/MarkMpn.Sql4Cds.Export.csproj b/MarkMpn.Sql4Cds.Export/MarkMpn.Sql4Cds.Export.csproj index 717d905a..ca9482f7 100644 --- a/MarkMpn.Sql4Cds.Export/MarkMpn.Sql4Cds.Export.csproj +++ b/MarkMpn.Sql4Cds.Export/MarkMpn.Sql4Cds.Export.csproj @@ -15,4 +15,8 @@ + + + + diff --git a/MarkMpn.Sql4Cds.XTB/SqlQueryControl.cs b/MarkMpn.Sql4Cds.XTB/SqlQueryControl.cs index dcfa1bc5..762d1587 100644 --- a/MarkMpn.Sql4Cds.XTB/SqlQueryControl.cs +++ b/MarkMpn.Sql4Cds.XTB/SqlQueryControl.cs @@ -1328,7 +1328,7 @@ private void FormatCell(object sender, DataGridViewCellFormattingEventArgs e) if (e.CellStyle != null) e.CellStyle.BackColor = Color.FromArgb(0xff, 0xff, 0xe1); } - else if (e.Value is SqlEntityReference || e.Value is SqlXml) + else if (cell.RawObject is SqlEntityReference || cell.RawObject is SqlXml) { if (e.CellStyle != null) { @@ -1894,17 +1894,20 @@ private void saveAsExcelToolStripMenuItem_Click(object sender, EventArgs e) if (saveDialog.ShowDialog() == DialogResult.OK) { - _exportBackgroundWorker.RunWorkerAsync(new ExportToExcel(table, saveDialog.FileName)); + _exportBackgroundWorker.RunWorkerAsync(new ExportToExcel(table, saveDialog.FileName, er => GetRecordUrl(er, out _))); } } } class ExportToExcel : ExportParams { - public ExportToExcel(DataTable table, string filename) + private readonly Func _urlGenerator; + + public ExportToExcel(DataTable table, string filename, Func urlGenerator) { DataTable = table; Filename = filename; + _urlGenerator = urlGenerator; } protected override IFileStreamFactory GetFileStreamFactory() @@ -1916,7 +1919,8 @@ protected override IFileStreamFactory GetFileStreamFactory() FilePath = Filename, IncludeHeaders = true, BoldHeaderRow = true - } + }, + UrlGenerator = _urlGenerator }; } } From 428f8e1067cc768bbab13dce79e8320e89452813 Mon Sep 17 00:00:00 2001 From: Mark Carrington <31017244+MarkMpn@users.noreply.github.com> Date: Mon, 1 Jul 2024 08:45:56 +0100 Subject: [PATCH 14/25] Added export handlers to ADS extension Made entity references into links in Markdown export --- .../Contracts/SaveResultsRequest.cs | 11 --- .../SaveAsMarkdownFileStreamFactory.cs | 9 ++- .../SaveAsMarkdownFileStreamWriter.cs | 18 ++++- .../Connection/ConnectionManager.cs | 19 +++++ .../Contracts/ExportRequests.cs | 36 ++++++++++ .../QueryExecution/QueryExecutionHandler.cs | 72 +++++++++++++++++++ 6 files changed, 150 insertions(+), 15 deletions(-) create mode 100644 MarkMpn.Sql4Cds.LanguageServer/QueryExecution/Contracts/ExportRequests.cs diff --git a/MarkMpn.Sql4Cds.Export/Contracts/SaveResultsRequest.cs b/MarkMpn.Sql4Cds.Export/Contracts/SaveResultsRequest.cs index 1663e791..b65eb1b1 100644 --- a/MarkMpn.Sql4Cds.Export/Contracts/SaveResultsRequest.cs +++ b/MarkMpn.Sql4Cds.Export/Contracts/SaveResultsRequest.cs @@ -179,16 +179,5 @@ public class SaveResultsAsXmlRequestParams: SaveResultsRequestParams /// public string Encoding { get; set; } } - - /// - /// Parameters for the save results result - /// - public class SaveResultRequestResult - { - /// - /// Error messages for saving to file. - /// - public string Messages { get; set; } - } } \ No newline at end of file diff --git a/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsMarkdownFileStreamFactory.cs b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsMarkdownFileStreamFactory.cs index ed159706..8171cd12 100644 --- a/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsMarkdownFileStreamFactory.cs +++ b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsMarkdownFileStreamFactory.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.IO; +using MarkMpn.Sql4Cds.Engine; using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; using Microsoft.SqlTools.ServiceLayer.Utility; @@ -14,14 +15,17 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage public class SaveAsMarkdownFileStreamFactory : IFileStreamFactory { private readonly SaveResultsAsMarkdownRequestParams _saveRequestParams; + private readonly Func _urlGenerator; /// /// Constructs and initializes a new instance of . /// /// Parameters for the save as request - public SaveAsMarkdownFileStreamFactory(SaveResultsAsMarkdownRequestParams requestParams) + public SaveAsMarkdownFileStreamFactory(SaveResultsAsMarkdownRequestParams requestParams, + Func urlGenerator) { this._saveRequestParams = requestParams; + this._urlGenerator = urlGenerator; } /// @@ -51,7 +55,8 @@ public IFileStreamWriter GetWriter(string fileName, IReadOnlyList diff --git a/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsMarkdownFileStreamWriter.cs b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsMarkdownFileStreamWriter.cs index 337a8c2b..5dd9bc22 100644 --- a/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsMarkdownFileStreamWriter.cs +++ b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsMarkdownFileStreamWriter.cs @@ -10,6 +10,7 @@ using System.Text; using System.Text.RegularExpressions; using System.Web; +using MarkMpn.Sql4Cds.Engine; using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage @@ -26,11 +27,13 @@ public partial class SaveAsMarkdownFileStreamWriter : SaveAsStreamWriter private readonly Encoding _encoding; private readonly string _lineSeparator; + private readonly Func _urlGenerator; public SaveAsMarkdownFileStreamWriter( Stream stream, SaveResultsAsMarkdownRequestParams requestParams, - IReadOnlyList columns) + IReadOnlyList columns, + Func urlGenerator) : base(stream, requestParams, columns) { // Parse the request params @@ -38,6 +41,7 @@ public SaveAsMarkdownFileStreamWriter( ? Environment.NewLine : requestParams.LineSeparator; this._encoding = ParseEncoding(requestParams.Encoding, Encoding.UTF8); + this._urlGenerator = urlGenerator; // Output the header if requested if (requestParams.IncludeHeaders) @@ -66,7 +70,7 @@ public override void WriteRow(IList row, IReadOnlyList selectedCells = row.Skip(this.ColumnStartIndex) .Take(this.ColumnCount) - .Select(c => EncodeMarkdownField(c.DisplayValue)); + .Select(c => EncodeMarkdownField(c)); string rowLine = string.Join(Delimiter, selectedCells); this.WriteLine($"{Delimiter}{rowLine}{Delimiter}"); @@ -94,6 +98,16 @@ internal static string EncodeMarkdownField(string field) return field; } + private string EncodeMarkdownField(DbCellValue value) + { + var encoded = EncodeMarkdownField(value.IsNull ? null : value.DisplayValue); + + if (!value.IsNull && value.RawObject is SqlEntityReference er) + encoded = $"[{encoded}]({_urlGenerator(er)})"; + + return encoded; + } + private void WriteLine(string line) { byte[] bytes = this._encoding.GetBytes(line + this._lineSeparator); diff --git a/MarkMpn.Sql4Cds.LanguageServer/Connection/ConnectionManager.cs b/MarkMpn.Sql4Cds.LanguageServer/Connection/ConnectionManager.cs index 45dd83b6..e29476b3 100644 --- a/MarkMpn.Sql4Cds.LanguageServer/Connection/ConnectionManager.cs +++ b/MarkMpn.Sql4Cds.LanguageServer/Connection/ConnectionManager.cs @@ -240,10 +240,13 @@ public bool ChangeConnection(string ownerUri, string newDatabase) class DataSourceWithInfo : DataSource { + private readonly string _url; + public DataSourceWithInfo(IOrganizationService org, string url, PersistentMetadataCache persistentMetadataCache) : base(org) { UniqueName = Name; ServerName = new Uri(url).Host; + _url = url; using (var con = new Sql4CdsConnection(new Dictionary { [Name] = this })) using (var cmd = con.CreateCommand()) @@ -279,6 +282,22 @@ public DataSourceWithInfo(IOrganizationService org, string url, PersistentMetada public string Version { get; set; } public string Username { get; set; } + + internal string GetEntityReferenceUrl(SqlEntityReference reference) + { + if (reference.IsNull) + { + return string.Empty; + } + var url = _url; + url = string.Concat(url, + url.EndsWith("/") ? "" : "/", + "main.aspx?etn=", + reference.LogicalName, + "&pagetype=entityrecord&id=", + reference.Id.ToString()); + return url; + } } class Session diff --git a/MarkMpn.Sql4Cds.LanguageServer/QueryExecution/Contracts/ExportRequests.cs b/MarkMpn.Sql4Cds.LanguageServer/QueryExecution/Contracts/ExportRequests.cs new file mode 100644 index 00000000..3e08a374 --- /dev/null +++ b/MarkMpn.Sql4Cds.LanguageServer/QueryExecution/Contracts/ExportRequests.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace MarkMpn.Sql4Cds.LanguageServer.QueryExecution.Contracts +{ + internal static class ExportRequests + { + public const string CsvMessageName = "query/saveCsv"; + public const string ExcelMessageName = "query/saveExcel"; + public const string JsonMessageName = "query/saveJson"; + public const string MarkdownMessageName = "query/saveMarkdown"; + public const string XmlMessageName = "query/saveXml"; + + public static readonly LspRequest CsvType = new LspRequest(CsvMessageName); + public static readonly LspRequest ExcelType = new LspRequest(ExcelMessageName); + public static readonly LspRequest JsonType = new LspRequest(JsonMessageName); + public static readonly LspRequest MarkdownType = new LspRequest(MarkdownMessageName); + public static readonly LspRequest XmlType = new LspRequest(XmlMessageName); + } + + /// + /// Parameters for the save results result + /// + public class SaveResultRequestResult + { + /// + /// Error messages for saving to file. + /// + public string Messages { get; set; } + } +} diff --git a/MarkMpn.Sql4Cds.LanguageServer/QueryExecution/QueryExecutionHandler.cs b/MarkMpn.Sql4Cds.LanguageServer/QueryExecution/QueryExecutionHandler.cs index a3c49a28..a80b71cb 100644 --- a/MarkMpn.Sql4Cds.LanguageServer/QueryExecution/QueryExecutionHandler.cs +++ b/MarkMpn.Sql4Cds.LanguageServer/QueryExecution/QueryExecutionHandler.cs @@ -21,6 +21,7 @@ using MarkMpn.Sql4Cds.LanguageServer.Workspace; using Microsoft.SqlTools.ServiceLayer.ExecutionPlan.Contracts; using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage; using Microsoft.VisualStudio.LanguageServer.Protocol; using Microsoft.Xrm.Sdk; using Microsoft.Xrm.Sdk.Metadata; @@ -58,6 +59,11 @@ public void Initialize(JsonRpc lsp) lsp.AddHandler(QueryExecutionPlanRequest.Type, HandleQueryExecutionPlan); lsp.AddHandler(QueryDisposeRequest.Type, HandleQueryDispose); lsp.AddHandler(ConfirmationResponse.Type, HandleConfirmation); + lsp.AddHandler(ExportRequests.CsvType, HandleExportCsv); + lsp.AddHandler(ExportRequests.ExcelType, HandleExportExcel); + lsp.AddHandler(ExportRequests.JsonType, HandleExportJson); + lsp.AddHandler(ExportRequests.MarkdownType, HandleExportMarkdown); + lsp.AddHandler(ExportRequests.XmlType, HandleExportXml); } private void HandleConfirmation(ConfirmationResponseParams arg) @@ -1049,5 +1055,71 @@ public QueryDisposeResult HandleQueryDispose(QueryDisposeParams request) _resultSets.Remove(request.OwnerUri, out _); return new QueryDisposeResult(); } + + public SaveResultRequestResult HandleExportCsv(SaveResultsAsCsvRequestParams request) + { + var factory = new SaveAsCsvFileStreamFactory { SaveRequestParams = request }; + return SaveResultsHelper(request, factory); + } + + public SaveResultRequestResult HandleExportExcel(SaveResultsAsExcelRequestParams request) + { + var factory = new SaveAsExcelFileStreamFactory + { + SaveRequestParams = request, + UrlGenerator = _connectionManager.GetConnection(request.OwnerUri).DataSource.GetEntityReferenceUrl + }; + return SaveResultsHelper(request, factory); + } + + public SaveResultRequestResult HandleExportJson(SaveResultsAsJsonRequestParams request) + { + var factory = new SaveAsJsonFileStreamFactory + { + SaveRequestParams = request + }; + return SaveResultsHelper(request, factory); + } + + public SaveResultRequestResult HandleExportMarkdown(SaveResultsAsMarkdownRequestParams request) + { + var factory = new SaveAsMarkdownFileStreamFactory(request, _connectionManager.GetConnection(request.OwnerUri).DataSource.GetEntityReferenceUrl); + return SaveResultsHelper(request, factory); + } + + public SaveResultRequestResult HandleExportXml(SaveResultsAsXmlRequestParams request) + { + var factory = new SaveAsXmlFileStreamFactory + { + SaveRequestParams = request + }; + return SaveResultsHelper(request, factory); + } + + private SaveResultRequestResult SaveResultsHelper(SaveResultsRequestParams request, IFileStreamFactory factory) + { + try + { + var resultSet = _resultSets[request.OwnerUri][request.ResultSetIndex]; + + using (var writer = factory.GetWriter(request.FilePath, resultSet.ColumnInfo)) + { + foreach (var row in resultSet.Values) + { + writer.WriteRow(row.Select((value, colIndex) => + { + var col = resultSet.ColumnInfo[colIndex]; + return ValueFormatter.Format(value, col.DataTypeName, col.NumericScale.GetValueOrDefault(), Sql4CdsSettings.Instance.LocalFormatDates); + }).ToArray(), resultSet.ColumnInfo); + } + } + + return new SaveResultRequestResult(); + } + catch (Exception ex) + { + return new SaveResultRequestResult { Messages = ex.Message }; + } + } } } From 69f2e020e84b01cae2baa37e912cf85ad55b1228 Mon Sep 17 00:00:00 2001 From: Mark Carrington <31017244+MarkMpn@users.noreply.github.com> Date: Mon, 1 Jul 2024 20:06:16 +0100 Subject: [PATCH 15/25] Updated namespace --- MarkMpn.Sql4Cds.Export/Contracts/DbCellValue.cs | 4 ++-- MarkMpn.Sql4Cds.Export/Contracts/DbColumnWrapper.cs | 4 ++-- MarkMpn.Sql4Cds.Export/Contracts/SaveResultsRequest.cs | 2 +- MarkMpn.Sql4Cds.Export/DataStorage/FileStreamReadResult.cs | 4 ++-- MarkMpn.Sql4Cds.Export/DataStorage/IFileStreamFactory.cs | 4 ++-- MarkMpn.Sql4Cds.Export/DataStorage/IFileStreamReader.cs | 4 ++-- MarkMpn.Sql4Cds.Export/DataStorage/IFileStreamWriter.cs | 4 ++-- .../DataStorage/SaveAsCsvFileStreamFactory.cs | 4 ++-- .../DataStorage/SaveAsCsvFileStreamWriter.cs | 4 ++-- .../DataStorage/SaveAsExcelFileStreamFactory.cs | 4 ++-- .../DataStorage/SaveAsExcelFileStreamWriter.cs | 4 ++-- .../DataStorage/SaveAsExcelFileStreamWriterHelper.cs | 4 ++-- .../DataStorage/SaveAsJsonFileStreamFactory.cs | 4 ++-- .../DataStorage/SaveAsJsonFileStreamWriter.cs | 4 ++-- .../DataStorage/SaveAsMarkdownFileStreamFactory.cs | 4 ++-- .../DataStorage/SaveAsMarkdownFileStreamWriter.cs | 4 ++-- MarkMpn.Sql4Cds.Export/DataStorage/SaveAsWriterBase.cs | 6 +++--- .../DataStorage/SaveAsXmlFileStreamFactory.cs | 4 ++-- .../DataStorage/SaveAsXmlFileStreamWriter.cs | 4 ++-- .../DataStorage/ServiceBufferFileStreamFactory.cs | 4 ++-- .../DataStorage/ServiceBufferFileStreamReader.cs | 6 +++--- .../DataStorage/ServiceBufferFileStreamWriter.cs | 6 +++--- MarkMpn.Sql4Cds.Export/DataStorage/StorageDataReader.cs | 6 +++--- MarkMpn.Sql4Cds.Export/README.md | 3 ++- MarkMpn.Sql4Cds.Export/SR.cs | 2 +- MarkMpn.Sql4Cds.Export/Utility/Extensions.cs | 2 +- MarkMpn.Sql4Cds.Export/Utility/Validate.cs | 2 +- MarkMpn.Sql4Cds.Export/ValueFormatter.cs | 4 ++-- .../QueryExecution/Contracts/ExportRequests.cs | 2 +- .../QueryExecution/Contracts/ResultSetSubset.cs | 2 +- .../QueryExecution/Contracts/ResultSetSummary.cs | 2 +- .../QueryExecution/QueryExecutionHandler.cs | 4 ++-- MarkMpn.Sql4Cds.XTB/SqlQueryControl.cs | 4 ++-- 33 files changed, 63 insertions(+), 62 deletions(-) diff --git a/MarkMpn.Sql4Cds.Export/Contracts/DbCellValue.cs b/MarkMpn.Sql4Cds.Export/Contracts/DbCellValue.cs index c3077b34..8435335d 100644 --- a/MarkMpn.Sql4Cds.Export/Contracts/DbCellValue.cs +++ b/MarkMpn.Sql4Cds.Export/Contracts/DbCellValue.cs @@ -3,9 +3,9 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using Microsoft.SqlTools.Utility; +using MarkMpn.Sql4Cds.Export.Utility; -namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts +namespace MarkMpn.Sql4Cds.Export.Contracts { /// /// Class used for internally passing results from a cell around. diff --git a/MarkMpn.Sql4Cds.Export/Contracts/DbColumnWrapper.cs b/MarkMpn.Sql4Cds.Export/Contracts/DbColumnWrapper.cs index a36ce8a3..6f297952 100644 --- a/MarkMpn.Sql4Cds.Export/Contracts/DbColumnWrapper.cs +++ b/MarkMpn.Sql4Cds.Export/Contracts/DbColumnWrapper.cs @@ -10,9 +10,9 @@ using System.Data.Common; using System.Data.SqlTypes; using System.Diagnostics; -using Microsoft.SqlTools.Utility; +using MarkMpn.Sql4Cds.Export.Utility; -namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts +namespace MarkMpn.Sql4Cds.Export.Contracts { /// /// Wrapper around a DbColumn, which provides extra functionality, but can be used as a diff --git a/MarkMpn.Sql4Cds.Export/Contracts/SaveResultsRequest.cs b/MarkMpn.Sql4Cds.Export/Contracts/SaveResultsRequest.cs index b65eb1b1..4139887c 100644 --- a/MarkMpn.Sql4Cds.Export/Contracts/SaveResultsRequest.cs +++ b/MarkMpn.Sql4Cds.Export/Contracts/SaveResultsRequest.cs @@ -4,7 +4,7 @@ // -namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts +namespace MarkMpn.Sql4Cds.Export.Contracts { /// /// Parameters for the save results request diff --git a/MarkMpn.Sql4Cds.Export/DataStorage/FileStreamReadResult.cs b/MarkMpn.Sql4Cds.Export/DataStorage/FileStreamReadResult.cs index 0939c9d4..9c23330d 100644 --- a/MarkMpn.Sql4Cds.Export/DataStorage/FileStreamReadResult.cs +++ b/MarkMpn.Sql4Cds.Export/DataStorage/FileStreamReadResult.cs @@ -3,9 +3,9 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using MarkMpn.Sql4Cds.Export.Contracts; -namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage +namespace MarkMpn.Sql4Cds.Export.DataStorage { /// /// Represents a value returned from a read from a file stream. This is used to eliminate ref diff --git a/MarkMpn.Sql4Cds.Export/DataStorage/IFileStreamFactory.cs b/MarkMpn.Sql4Cds.Export/DataStorage/IFileStreamFactory.cs index 63e34637..1bf48b80 100644 --- a/MarkMpn.Sql4Cds.Export/DataStorage/IFileStreamFactory.cs +++ b/MarkMpn.Sql4Cds.Export/DataStorage/IFileStreamFactory.cs @@ -4,9 +4,9 @@ // using System.Collections.Generic; -using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using MarkMpn.Sql4Cds.Export.Contracts; -namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage +namespace MarkMpn.Sql4Cds.Export.DataStorage { /// /// Interface for a factory that creates filesystem readers/writers diff --git a/MarkMpn.Sql4Cds.Export/DataStorage/IFileStreamReader.cs b/MarkMpn.Sql4Cds.Export/DataStorage/IFileStreamReader.cs index 1ecc5c0b..926d0893 100644 --- a/MarkMpn.Sql4Cds.Export/DataStorage/IFileStreamReader.cs +++ b/MarkMpn.Sql4Cds.Export/DataStorage/IFileStreamReader.cs @@ -5,9 +5,9 @@ using System; using System.Collections.Generic; -using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using MarkMpn.Sql4Cds.Export.Contracts; -namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage +namespace MarkMpn.Sql4Cds.Export.DataStorage { /// /// Interface for a object that reads from the filesystem diff --git a/MarkMpn.Sql4Cds.Export/DataStorage/IFileStreamWriter.cs b/MarkMpn.Sql4Cds.Export/DataStorage/IFileStreamWriter.cs index dd5b0311..862f351d 100644 --- a/MarkMpn.Sql4Cds.Export/DataStorage/IFileStreamWriter.cs +++ b/MarkMpn.Sql4Cds.Export/DataStorage/IFileStreamWriter.cs @@ -6,10 +6,10 @@ using System; using System.Collections.Generic; using MarkMpn.Sql4Cds.Engine; -using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using MarkMpn.Sql4Cds.Export.Contracts; -namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage +namespace MarkMpn.Sql4Cds.Export.DataStorage { /// /// Interface for a object that writes to a filesystem wrapper diff --git a/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsCsvFileStreamFactory.cs b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsCsvFileStreamFactory.cs index 674599ea..5a5a585c 100644 --- a/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsCsvFileStreamFactory.cs +++ b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsCsvFileStreamFactory.cs @@ -6,10 +6,10 @@ using System; using System.Collections.Generic; using System.IO; -using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using MarkMpn.Sql4Cds.Export.Contracts; using Microsoft.SqlTools.ServiceLayer.Utility; -namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage +namespace MarkMpn.Sql4Cds.Export.DataStorage { /// /// Factory for creating a reader/writer pair that will read from the temporary buffer file diff --git a/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsCsvFileStreamWriter.cs b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsCsvFileStreamWriter.cs index 1a12375e..135c5907 100644 --- a/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsCsvFileStreamWriter.cs +++ b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsCsvFileStreamWriter.cs @@ -8,9 +8,9 @@ using System.IO; using System.Linq; using System.Text; -using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using MarkMpn.Sql4Cds.Export.Contracts; -namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage +namespace MarkMpn.Sql4Cds.Export.DataStorage { /// /// Writer for writing rows of results to a CSV file diff --git a/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsExcelFileStreamFactory.cs b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsExcelFileStreamFactory.cs index 961769f9..81304023 100644 --- a/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsExcelFileStreamFactory.cs +++ b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsExcelFileStreamFactory.cs @@ -7,10 +7,10 @@ using System.Collections.Generic; using System.IO; using MarkMpn.Sql4Cds.Engine; -using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using MarkMpn.Sql4Cds.Export.Contracts; using Microsoft.SqlTools.ServiceLayer.Utility; -namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage +namespace MarkMpn.Sql4Cds.Export.DataStorage { /// /// Factory for creating a reader/writer pair that will read from the temporary buffer file diff --git a/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsExcelFileStreamWriter.cs b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsExcelFileStreamWriter.cs index b2779ad0..b72f3ae4 100644 --- a/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsExcelFileStreamWriter.cs +++ b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsExcelFileStreamWriter.cs @@ -7,10 +7,10 @@ using System.Collections.Generic; using System.IO; using MarkMpn.Sql4Cds.Engine; -using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using MarkMpn.Sql4Cds.Export.Contracts; using SkiaSharp; -namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage +namespace MarkMpn.Sql4Cds.Export.DataStorage { /// /// Writer for writing rows of results to a Excel file diff --git a/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsExcelFileStreamWriterHelper.cs b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsExcelFileStreamWriterHelper.cs index 2acf7c24..d2919ecc 100644 --- a/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsExcelFileStreamWriterHelper.cs +++ b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsExcelFileStreamWriterHelper.cs @@ -11,9 +11,9 @@ using System.IO.Compression; using System.Xml; using MarkMpn.Sql4Cds.Engine; -using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using MarkMpn.Sql4Cds.Export.Contracts; -namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage +namespace MarkMpn.Sql4Cds.Export.DataStorage { // A xlsx file is a zip with specific folder structure. // http://www.ecma-international.org/publications/standards/Ecma-376.htm diff --git a/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsJsonFileStreamFactory.cs b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsJsonFileStreamFactory.cs index 4abf5ea3..4f79fb3a 100644 --- a/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsJsonFileStreamFactory.cs +++ b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsJsonFileStreamFactory.cs @@ -6,10 +6,10 @@ using System; using System.Collections.Generic; using System.IO; -using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using MarkMpn.Sql4Cds.Export.Contracts; using Microsoft.SqlTools.ServiceLayer.Utility; -namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage +namespace MarkMpn.Sql4Cds.Export.DataStorage { public class SaveAsJsonFileStreamFactory : IFileStreamFactory { diff --git a/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsJsonFileStreamWriter.cs b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsJsonFileStreamWriter.cs index 9eb302e8..e521d4df 100644 --- a/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsJsonFileStreamWriter.cs +++ b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsJsonFileStreamWriter.cs @@ -6,10 +6,10 @@ using System; using System.Collections.Generic; using System.IO; -using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using MarkMpn.Sql4Cds.Export.Contracts; using Newtonsoft.Json; -namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage +namespace MarkMpn.Sql4Cds.Export.DataStorage { /// /// Writer for writing rows of results to a JSON file. diff --git a/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsMarkdownFileStreamFactory.cs b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsMarkdownFileStreamFactory.cs index 8171cd12..6276b65c 100644 --- a/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsMarkdownFileStreamFactory.cs +++ b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsMarkdownFileStreamFactory.cs @@ -7,10 +7,10 @@ using System.Collections.Generic; using System.IO; using MarkMpn.Sql4Cds.Engine; -using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using MarkMpn.Sql4Cds.Export.Contracts; using Microsoft.SqlTools.ServiceLayer.Utility; -namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage +namespace MarkMpn.Sql4Cds.Export.DataStorage { public class SaveAsMarkdownFileStreamFactory : IFileStreamFactory { diff --git a/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsMarkdownFileStreamWriter.cs b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsMarkdownFileStreamWriter.cs index 5dd9bc22..eb7131fb 100644 --- a/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsMarkdownFileStreamWriter.cs +++ b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsMarkdownFileStreamWriter.cs @@ -11,9 +11,9 @@ using System.Text.RegularExpressions; using System.Web; using MarkMpn.Sql4Cds.Engine; -using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using MarkMpn.Sql4Cds.Export.Contracts; -namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage +namespace MarkMpn.Sql4Cds.Export.DataStorage { /// /// Writer for exporting results to a Markdown table. diff --git a/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsWriterBase.cs b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsWriterBase.cs index e3248ec8..dec97d3f 100644 --- a/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsWriterBase.cs +++ b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsWriterBase.cs @@ -7,10 +7,10 @@ using System.Collections.Generic; using System.IO; using System.Text; -using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; -using Microsoft.SqlTools.Utility; +using MarkMpn.Sql4Cds.Export.Contracts; +using MarkMpn.Sql4Cds.Export.Utility; -namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage +namespace MarkMpn.Sql4Cds.Export.DataStorage { /// /// Abstract class for implementing writers that save results to file. Stores some basic info diff --git a/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsXmlFileStreamFactory.cs b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsXmlFileStreamFactory.cs index 3eda9221..19d86789 100644 --- a/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsXmlFileStreamFactory.cs +++ b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsXmlFileStreamFactory.cs @@ -6,10 +6,10 @@ using System; using System.Collections.Generic; using System.IO; -using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using MarkMpn.Sql4Cds.Export.Contracts; using Microsoft.SqlTools.ServiceLayer.Utility; -namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage +namespace MarkMpn.Sql4Cds.Export.DataStorage { public class SaveAsXmlFileStreamFactory : IFileStreamFactory { diff --git a/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsXmlFileStreamWriter.cs b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsXmlFileStreamWriter.cs index 0698525e..1f9a20f1 100644 --- a/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsXmlFileStreamWriter.cs +++ b/MarkMpn.Sql4Cds.Export/DataStorage/SaveAsXmlFileStreamWriter.cs @@ -8,9 +8,9 @@ using System.IO; using System.Text; using System.Xml; -using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using MarkMpn.Sql4Cds.Export.Contracts; -namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage +namespace MarkMpn.Sql4Cds.Export.DataStorage { /// /// Writer for writing rows of results to a XML file. diff --git a/MarkMpn.Sql4Cds.Export/DataStorage/ServiceBufferFileStreamFactory.cs b/MarkMpn.Sql4Cds.Export/DataStorage/ServiceBufferFileStreamFactory.cs index d58ad9de..2262211c 100644 --- a/MarkMpn.Sql4Cds.Export/DataStorage/ServiceBufferFileStreamFactory.cs +++ b/MarkMpn.Sql4Cds.Export/DataStorage/ServiceBufferFileStreamFactory.cs @@ -5,10 +5,10 @@ using System.Collections.Generic; using System.IO; -using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using MarkMpn.Sql4Cds.Export.Contracts; using Microsoft.SqlTools.ServiceLayer.Utility; -namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage +namespace MarkMpn.Sql4Cds.Export.DataStorage { /// /// Factory that creates file reader/writers that process rows in an internal, non-human readable file format diff --git a/MarkMpn.Sql4Cds.Export/DataStorage/ServiceBufferFileStreamReader.cs b/MarkMpn.Sql4Cds.Export/DataStorage/ServiceBufferFileStreamReader.cs index 709aec37..4a7b462e 100644 --- a/MarkMpn.Sql4Cds.Export/DataStorage/ServiceBufferFileStreamReader.cs +++ b/MarkMpn.Sql4Cds.Export/DataStorage/ServiceBufferFileStreamReader.cs @@ -9,10 +9,10 @@ using System.Data.SqlTypes; using System.IO; using System.Text; -using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; -using Microsoft.SqlTools.Utility; +using MarkMpn.Sql4Cds.Export.Contracts; +using MarkMpn.Sql4Cds.Export.Utility; -namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage +namespace MarkMpn.Sql4Cds.Export.DataStorage { /// /// Reader for service buffer formatted file streams diff --git a/MarkMpn.Sql4Cds.Export/DataStorage/ServiceBufferFileStreamWriter.cs b/MarkMpn.Sql4Cds.Export/DataStorage/ServiceBufferFileStreamWriter.cs index 75f0589b..2fa2205d 100644 --- a/MarkMpn.Sql4Cds.Export/DataStorage/ServiceBufferFileStreamWriter.cs +++ b/MarkMpn.Sql4Cds.Export/DataStorage/ServiceBufferFileStreamWriter.cs @@ -9,11 +9,11 @@ using System.Diagnostics; using System.IO; using System.Text; -using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using MarkMpn.Sql4Cds.Export.Contracts; using Microsoft.SqlTools.ServiceLayer.Utility; -using Microsoft.SqlTools.Utility; +using MarkMpn.Sql4Cds.Export.Utility; -namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage +namespace MarkMpn.Sql4Cds.Export.DataStorage { /// /// Writer for service buffer formatted file streams diff --git a/MarkMpn.Sql4Cds.Export/DataStorage/StorageDataReader.cs b/MarkMpn.Sql4Cds.Export/DataStorage/StorageDataReader.cs index 4677f90d..ff3c8ddd 100644 --- a/MarkMpn.Sql4Cds.Export/DataStorage/StorageDataReader.cs +++ b/MarkMpn.Sql4Cds.Export/DataStorage/StorageDataReader.cs @@ -10,10 +10,10 @@ using System.IO; using System.Linq; using System.Xml; -using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; -using Microsoft.SqlTools.Utility; +using MarkMpn.Sql4Cds.Export.Contracts; +using MarkMpn.Sql4Cds.Export.Utility; -namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage +namespace MarkMpn.Sql4Cds.Export.DataStorage { /// /// Wrapper around a DbData reader to perform some special operations more simply diff --git a/MarkMpn.Sql4Cds.Export/README.md b/MarkMpn.Sql4Cds.Export/README.md index 486f93c6..ae8ef8b5 100644 --- a/MarkMpn.Sql4Cds.Export/README.md +++ b/MarkMpn.Sql4Cds.Export/README.md @@ -1,2 +1,3 @@ A fork of various classes from https://github.com/microsoft/sqltoolsservice to handle storing and exporting -data from a data reader to a file in various different formats. \ No newline at end of file +data from a data reader to a file in various different formats. Extended to handle SqlEntityReference values +as hyperlinks in Excel and Markdown exports. \ No newline at end of file diff --git a/MarkMpn.Sql4Cds.Export/SR.cs b/MarkMpn.Sql4Cds.Export/SR.cs index f9955f58..8796c01f 100644 --- a/MarkMpn.Sql4Cds.Export/SR.cs +++ b/MarkMpn.Sql4Cds.Export/SR.cs @@ -3,7 +3,7 @@ using System.Runtime.Serialization; using System.Text; -namespace Microsoft.SqlTools.ServiceLayer.QueryExecution +namespace MarkMpn.Sql4Cds.Export { internal class SR { diff --git a/MarkMpn.Sql4Cds.Export/Utility/Extensions.cs b/MarkMpn.Sql4Cds.Export/Utility/Extensions.cs index 70daac58..6068d107 100644 --- a/MarkMpn.Sql4Cds.Export/Utility/Extensions.cs +++ b/MarkMpn.Sql4Cds.Export/Utility/Extensions.cs @@ -6,7 +6,7 @@ using System; using System.Collections.Generic; -namespace Microsoft.SqlTools.Utility +namespace MarkMpn.Sql4Cds.Export.Utility { public static class ObjectExtensions { diff --git a/MarkMpn.Sql4Cds.Export/Utility/Validate.cs b/MarkMpn.Sql4Cds.Export/Utility/Validate.cs index 1b3d4074..f5181aca 100644 --- a/MarkMpn.Sql4Cds.Export/Utility/Validate.cs +++ b/MarkMpn.Sql4Cds.Export/Utility/Validate.cs @@ -6,7 +6,7 @@ using System; using System.Collections.Generic; -namespace Microsoft.SqlTools.Utility +namespace MarkMpn.Sql4Cds.Export.Utility { /// /// Provides common validation methods to simplify method diff --git a/MarkMpn.Sql4Cds.Export/ValueFormatter.cs b/MarkMpn.Sql4Cds.Export/ValueFormatter.cs index 130efb2e..7bc6603a 100644 --- a/MarkMpn.Sql4Cds.Export/ValueFormatter.cs +++ b/MarkMpn.Sql4Cds.Export/ValueFormatter.cs @@ -3,8 +3,8 @@ using System.Data.Common; using System.Data.SqlTypes; using System.Text; -using Microsoft.SqlTools.ServiceLayer.QueryExecution; -using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using MarkMpn.Sql4Cds.Export; +using MarkMpn.Sql4Cds.Export.Contracts; namespace MarkMpn.Sql4Cds.Export { diff --git a/MarkMpn.Sql4Cds.LanguageServer/QueryExecution/Contracts/ExportRequests.cs b/MarkMpn.Sql4Cds.LanguageServer/QueryExecution/Contracts/ExportRequests.cs index 3e08a374..38ce709d 100644 --- a/MarkMpn.Sql4Cds.LanguageServer/QueryExecution/Contracts/ExportRequests.cs +++ b/MarkMpn.Sql4Cds.LanguageServer/QueryExecution/Contracts/ExportRequests.cs @@ -3,7 +3,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using MarkMpn.Sql4Cds.Export.Contracts; using Microsoft.VisualStudio.LanguageServer.Protocol; namespace MarkMpn.Sql4Cds.LanguageServer.QueryExecution.Contracts diff --git a/MarkMpn.Sql4Cds.LanguageServer/QueryExecution/Contracts/ResultSetSubset.cs b/MarkMpn.Sql4Cds.LanguageServer/QueryExecution/Contracts/ResultSetSubset.cs index 8ead94cd..58669e3c 100644 --- a/MarkMpn.Sql4Cds.LanguageServer/QueryExecution/Contracts/ResultSetSubset.cs +++ b/MarkMpn.Sql4Cds.LanguageServer/QueryExecution/Contracts/ResultSetSubset.cs @@ -1,4 +1,4 @@ -using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using MarkMpn.Sql4Cds.Export.Contracts; namespace MarkMpn.Sql4Cds.LanguageServer.QueryExecution.Contracts { diff --git a/MarkMpn.Sql4Cds.LanguageServer/QueryExecution/Contracts/ResultSetSummary.cs b/MarkMpn.Sql4Cds.LanguageServer/QueryExecution/Contracts/ResultSetSummary.cs index c0ed6bf0..ef40153a 100644 --- a/MarkMpn.Sql4Cds.LanguageServer/QueryExecution/Contracts/ResultSetSummary.cs +++ b/MarkMpn.Sql4Cds.LanguageServer/QueryExecution/Contracts/ResultSetSummary.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using MarkMpn.Sql4Cds.Export.Contracts; namespace MarkMpn.Sql4Cds.LanguageServer.QueryExecution.Contracts { diff --git a/MarkMpn.Sql4Cds.LanguageServer/QueryExecution/QueryExecutionHandler.cs b/MarkMpn.Sql4Cds.LanguageServer/QueryExecution/QueryExecutionHandler.cs index a80b71cb..9822533e 100644 --- a/MarkMpn.Sql4Cds.LanguageServer/QueryExecution/QueryExecutionHandler.cs +++ b/MarkMpn.Sql4Cds.LanguageServer/QueryExecution/QueryExecutionHandler.cs @@ -20,8 +20,8 @@ using MarkMpn.Sql4Cds.LanguageServer.QueryExecution.Contracts; using MarkMpn.Sql4Cds.LanguageServer.Workspace; using Microsoft.SqlTools.ServiceLayer.ExecutionPlan.Contracts; -using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; -using Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage; +using MarkMpn.Sql4Cds.Export.Contracts; +using MarkMpn.Sql4Cds.Export.DataStorage; using Microsoft.VisualStudio.LanguageServer.Protocol; using Microsoft.Xrm.Sdk; using Microsoft.Xrm.Sdk.Metadata; diff --git a/MarkMpn.Sql4Cds.XTB/SqlQueryControl.cs b/MarkMpn.Sql4Cds.XTB/SqlQueryControl.cs index 762d1587..25715ae6 100644 --- a/MarkMpn.Sql4Cds.XTB/SqlQueryControl.cs +++ b/MarkMpn.Sql4Cds.XTB/SqlQueryControl.cs @@ -19,11 +19,11 @@ using MarkMpn.Sql4Cds.Engine; using MarkMpn.Sql4Cds.Engine.ExecutionPlan; using MarkMpn.Sql4Cds.Export; +using MarkMpn.Sql4Cds.Export.Contracts; +using MarkMpn.Sql4Cds.Export.DataStorage; using McTools.Xrm.Connection; using Microsoft.ApplicationInsights; using Microsoft.SqlServer.TransactSql.ScriptDom; -using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; -using Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage; using Microsoft.Xrm.Sdk; using Microsoft.Xrm.Sdk.Query; using Microsoft.Xrm.Tooling.Connector; From b7df402dc1f63364ffc87972807a0e5eae8f39ee Mon Sep 17 00:00:00 2001 From: Mark Carrington <31017244+MarkMpn@users.noreply.github.com> Date: Tue, 2 Jul 2024 23:40:03 +0100 Subject: [PATCH 16/25] Do not use attributes from semi-joins for custom paging. Fixes #498 --- .../ExecutionPlanTests.cs | 98 +++++++++++++++++++ .../ExecutionPlan/FetchXmlScan.cs | 55 +++++++++-- .../MarkMpn.Sql4Cds.XTB.csproj | 3 +- MarkMpn.Sql4Cds/MarkMpn.Sql4Cds.csproj | 9 +- 4 files changed, 152 insertions(+), 13 deletions(-) diff --git a/MarkMpn.Sql4Cds.Engine.Tests/ExecutionPlanTests.cs b/MarkMpn.Sql4Cds.Engine.Tests/ExecutionPlanTests.cs index 7d27464d..9c8d0590 100644 --- a/MarkMpn.Sql4Cds.Engine.Tests/ExecutionPlanTests.cs +++ b/MarkMpn.Sql4Cds.Engine.Tests/ExecutionPlanTests.cs @@ -7938,6 +7938,104 @@ public void VirtualAttributeAliases() +"); + } + + [TestMethod] + public void CustomPagingWithInSubquery() + { + var query = @" +SELECT DISTINCT + c.contactid, + CASE + WHEN c.lastname = c.firstname THEN 1 + ELSE 0 + END AS flag +FROM account as a +inner join contact AS c on c.contactid = a.primarycontactid +LEFT JOIN account AS ca ON c.contactid = ca.primarycontactid +WHERE c.parentcustomerid IN (SELECT contactid FROM contact WHERE createdon = today())"; + + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); + + var plans = planBuilder.Build(query, null, out _); + + Assert.AreEqual(1, plans.Length); + + var select = AssertNode(plans[0]); + var distinct = AssertNode(select.Source); + var compute = AssertNode(distinct.Source); + var fetch = AssertNode(compute.Source); + Assert.IsTrue(fetch.UsingCustomPaging); + AssertFetchXml(fetch, @" + + + + + + + + + + + + + + + + + + + + +"); + } + + [TestMethod] + public void InSubqueryOnPrimaryKeyFoldedToFilter() + { + var query = @" +SELECT DISTINCT + c.contactid, + CASE + WHEN c.lastname = c.firstname THEN 1 + ELSE 0 + END AS flag +FROM account as a +inner join contact AS c on c.contactid = a.primarycontactid +LEFT JOIN account AS ca ON c.contactid = ca.primarycontactid +WHERE c.contactid IN (SELECT contactid FROM contact WHERE createdon = today())"; + + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); + + var plans = planBuilder.Build(query, null, out _); + + Assert.AreEqual(1, plans.Length); + + var select = AssertNode(plans[0]); + var distinct = AssertNode(select.Source); + var compute = AssertNode(distinct.Source); + var fetch = AssertNode(compute.Source); + Assert.IsTrue(fetch.UsingCustomPaging); + AssertFetchXml(fetch, @" + + + + + + + + + + + + + + + + + + "); } } diff --git a/MarkMpn.Sql4Cds.Engine/ExecutionPlan/FetchXmlScan.cs b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/FetchXmlScan.cs index 44a7a1ed..01d375ce 100644 --- a/MarkMpn.Sql4Cds.Engine/ExecutionPlan/FetchXmlScan.cs +++ b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/FetchXmlScan.cs @@ -213,7 +213,7 @@ public bool RequiresCustomPaging(IDictionary dataSources) foreach (var linkEntity in Entity.GetLinkEntities()) { // Link entities used for filtering do not require custom paging - if (linkEntity.linktype == "exists" || linkEntity.linktype == "in") + if (linkEntity.linktype == "exists" || linkEntity.linktype == "in" || linkEntity.SemiJoin) continue; // Sorts on link entities always require custom paging @@ -1357,7 +1357,7 @@ private string AddSuffix(string name, string suffix) public override IDataExecutionPlanNodeInternal FoldQuery(NodeCompilationContext context, IList hints) { - NormalizeFilters(); + NormalizeFilters(context); if (hints != null) { @@ -1390,7 +1390,7 @@ public override IDataExecutionPlanNodeInternal FoldQuery(NodeCompilationContext if (FoldFilterToIndexSpool(context, out var indexSpool)) { - NormalizeFilters(); + NormalizeFilters(context); Parent = indexSpool; return indexSpool.FoldQuery(context, hints); } @@ -1580,8 +1580,9 @@ private bool GetBypassPluginExecution(NodeCompilationContext context, IList(); + + foreach (var item in items) + { + if (!(item is FetchLinkEntityType linkEntity)) + { + newItems.Add(item); + continue; + } + + linkEntity.Items = RemoveIdentitySemiJoinLinkEntities(linkEntity.name, metadata, linkEntity.Items); + + if (linkEntity.linktype != "inner" || + linkEntity.name != logicalName || + !linkEntity.SemiJoin || + linkEntity.from != metadata[logicalName].PrimaryIdAttribute || + linkEntity.to != metadata[logicalName].PrimaryIdAttribute) + { + newItems.Add(item); + continue; + } + + if (linkEntity.Items != null) + newItems.AddRange(linkEntity.Items); + } + + return newItems.ToArray(); + } + private void MoveFiltersToLinkEntities() { // If we've got AND-ed conditions that have an entityname that refers to an inner-joined link entity, move @@ -1886,7 +1929,7 @@ public override void AddRequiredColumns(NodeCompilationContext context, IList le.linktype != "exists" && le.linktype != "in" && !HasSingleRecordFilter(le, dataSource.Metadata[le.name].PrimaryIdAttribute))) + foreach (var linkEntity in Entity.GetLinkEntities().Where(le => le.linktype != "exists" && le.linktype != "in" && !le.SemiJoin && !HasSingleRecordFilter(le, dataSource.Metadata[le.name].PrimaryIdAttribute))) AddAllDistinctAttributes(linkEntity, dataSource); } else @@ -1896,7 +1939,7 @@ public override void AddRequiredColumns(NodeCompilationContext context, IList le.linktype != "exists" && le.linktype != "in" && !HasSingleRecordFilter(le, dataSource.Metadata[le.name].PrimaryIdAttribute))) + foreach (var linkEntity in Entity.GetLinkEntities().Where(le => le.linktype != "exists" && le.linktype != "in" && !le.SemiJoin && !HasSingleRecordFilter(le, dataSource.Metadata[le.name].PrimaryIdAttribute))) AddPrimaryIdAttribute(linkEntity, dataSource); } } diff --git a/MarkMpn.Sql4Cds.XTB/MarkMpn.Sql4Cds.XTB.csproj b/MarkMpn.Sql4Cds.XTB/MarkMpn.Sql4Cds.XTB.csproj index d989849e..ce0ed64f 100644 --- a/MarkMpn.Sql4Cds.XTB/MarkMpn.Sql4Cds.XTB.csproj +++ b/MarkMpn.Sql4Cds.XTB/MarkMpn.Sql4Cds.XTB.csproj @@ -314,7 +314,8 @@ copy $(TargetDir)MarkMpn.Sql4Cds.XTB.dll %25appdata%25\MscrmTools\XrmToolBox\Plugins\MarkMpn.Sql4Cds - copy $(TargetDir)MarkMpn.Sql4Cds.XTB.dll %25appdata%25\MscrmTools\XrmToolBox\Plugins\MarkMpn.Sql4Cds + copy $(TargetDir)MarkMpn.Sql4Cds.Engine.dll %25appdata%25\MscrmTools\XrmToolBox\Plugins\MarkMpn.Sql4Cds +copy $(TargetDir)MarkMpn.Sql4Cds.XTB.dll %25appdata%25\MscrmTools\XrmToolBox\Plugins\MarkMpn.Sql4Cds copy $(TargetDir)MarkMpn.Sql4Cds.Export.dll %25appdata%25\MscrmTools\XrmToolBox\Plugins\MarkMpn.Sql4Cds copy $(TargetDir)SkiaSharp.dll %25appdata%25\MscrmTools\XrmToolBox\Plugins\MarkMpn.Sql4Cds copy $(TargetDir)System.Text.Encoding.CodePages.dll %25appdata%25\MscrmTools\XrmToolBox\Plugins\MarkMpn.Sql4Cds diff --git a/MarkMpn.Sql4Cds/MarkMpn.Sql4Cds.csproj b/MarkMpn.Sql4Cds/MarkMpn.Sql4Cds.csproj index cbc4180c..2de50353 100644 --- a/MarkMpn.Sql4Cds/MarkMpn.Sql4Cds.csproj +++ b/MarkMpn.Sql4Cds/MarkMpn.Sql4Cds.csproj @@ -46,9 +46,6 @@ - - False - @@ -80,9 +77,9 @@ {04c2d073-de54-4628-b876-5965d0b75b6e} MarkMpn.Sql4Cds.Controls - - {c77b731d-e55c-4197-b96c-2b23eb9f56ef} - MarkMpn.Sql4Cds.Engine + + {920bac0a-847b-467b-adaa-674df59209f2} + MarkMpn.Sql4Cds.Export From 5acb9e0891781e7810cc22db414bd744f46605c5 Mon Sep 17 00:00:00 2001 From: Mark Carrington <31017244+MarkMpn@users.noreply.github.com> Date: Wed, 3 Jul 2024 08:39:29 +0100 Subject: [PATCH 17/25] Added using --- MarkMpn.Sql4Cds.Engine/AssemblyInfo.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MarkMpn.Sql4Cds.Engine/AssemblyInfo.cs b/MarkMpn.Sql4Cds.Engine/AssemblyInfo.cs index ea6c5af7..4418a995 100644 --- a/MarkMpn.Sql4Cds.Engine/AssemblyInfo.cs +++ b/MarkMpn.Sql4Cds.Engine/AssemblyInfo.cs @@ -1,3 +1,4 @@ -using System.Runtime.CompilerServices; +using System.Reflection; +using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("MarkMpn.Sql4Cds.Engine.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c13706e77bd5be4fad5e04b334cb05a1a8d7fb12baea9b44579760cd26dec6e6d0f5496a2f7933cf44a172a0dc2bccbd94f090bc7f8b79fd76e244840f8447389d6d6bcbab1cf9085d1043140346ecd9f954a523a82861c596214ae0b92537d6dc6796ee649239684d66e45aada225102503a9f8c4034ab8e0a8bbadf9d210a8")] \ No newline at end of file From 5341e33206a3db62e2a2b2ddca495a4d03870ea5 Mon Sep 17 00:00:00 2001 From: Mark Carrington <31017244+MarkMpn@users.noreply.github.com> Date: Wed, 3 Jul 2024 08:49:55 +0100 Subject: [PATCH 18/25] Updated build process --- .../{AssemblyInfo.cs => AssemblyAttributes.cs} | 3 +-- pr-build.yml | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) rename MarkMpn.Sql4Cds.Engine/{AssemblyInfo.cs => AssemblyAttributes.cs} (85%) diff --git a/MarkMpn.Sql4Cds.Engine/AssemblyInfo.cs b/MarkMpn.Sql4Cds.Engine/AssemblyAttributes.cs similarity index 85% rename from MarkMpn.Sql4Cds.Engine/AssemblyInfo.cs rename to MarkMpn.Sql4Cds.Engine/AssemblyAttributes.cs index 4418a995..ea6c5af7 100644 --- a/MarkMpn.Sql4Cds.Engine/AssemblyInfo.cs +++ b/MarkMpn.Sql4Cds.Engine/AssemblyAttributes.cs @@ -1,4 +1,3 @@ -using System.Reflection; -using System.Runtime.CompilerServices; +using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("MarkMpn.Sql4Cds.Engine.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c13706e77bd5be4fad5e04b334cb05a1a8d7fb12baea9b44579760cd26dec6e6d0f5496a2f7933cf44a172a0dc2bccbd94f090bc7f8b79fd76e244840f8447389d6d6bcbab1cf9085d1043140346ecd9f954a523a82861c596214ae0b92537d6dc6796ee649239684d66e45aada225102503a9f8c4034ab8e0a8bbadf9d210a8")] \ No newline at end of file diff --git a/pr-build.yml b/pr-build.yml index bad453b6..6bd6ea30 100644 --- a/pr-build.yml +++ b/pr-build.yml @@ -20,7 +20,7 @@ steps: command: custom arguments: install GitVersion.CommandLine -Version 4.0.0 -OutputDirectory $(Build.BinariesDirectory)/tools -ExcludeVersion -- script: $(Build.BinariesDirectory)/tools/GitVersion.CommandLine/tools/GitVersion.exe /output buildserver /nofetch /updateassemblyinfo +- script: $(Build.BinariesDirectory)/tools/GitVersion.CommandLine/tools/GitVersion.exe /output buildserver /nofetch /updateassemblyinfo /updateprojectfiles displayName: Determine Version - task: PowerShell@2 From f1386130b9c3d49fd161f9ef45b160e277997ea9 Mon Sep 17 00:00:00 2001 From: Mark Carrington <31017244+MarkMpn@users.noreply.github.com> Date: Wed, 3 Jul 2024 09:04:09 +0100 Subject: [PATCH 19/25] Use new gitversion task --- pr-build.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/pr-build.yml b/pr-build.yml index 6bd6ea30..970ee790 100644 --- a/pr-build.yml +++ b/pr-build.yml @@ -14,20 +14,21 @@ steps: - checkout: self persistCredentials: true -- task: NuGetCommand@2 - displayName: Install GitVersion +- task: gitversion/setup@0 + displayName: 'Install GitVersion' inputs: - command: custom - arguments: install GitVersion.CommandLine -Version 4.0.0 -OutputDirectory $(Build.BinariesDirectory)/tools -ExcludeVersion + versionSpec: '5.x' -- script: $(Build.BinariesDirectory)/tools/GitVersion.CommandLine/tools/GitVersion.exe /output buildserver /nofetch /updateassemblyinfo /updateprojectfiles - displayName: Determine Version +- task: gitversion/execute@0 + displayName: 'Determine Version' + inputs: + updateAssemblyInfo: true - task: PowerShell@2 displayName: Update version in the vsix manifest inputs: filePath: 'MarkMpn.Sql4Cds.SSMS\update-version.ps1' - arguments: '$(GitVersion.AssemblySemVer)' + arguments: '$(assemblySemVer)' pwsh: true - task: DotNetCoreCLI@2 From 97ee289a5301ff09001989bb81bd8024f6e19bbd Mon Sep 17 00:00:00 2001 From: Mark Carrington <31017244+MarkMpn@users.noreply.github.com> Date: Wed, 3 Jul 2024 09:11:06 +0100 Subject: [PATCH 20/25] Updated gitversion task versions --- pr-build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pr-build.yml b/pr-build.yml index 970ee790..a7e61687 100644 --- a/pr-build.yml +++ b/pr-build.yml @@ -14,12 +14,12 @@ steps: - checkout: self persistCredentials: true -- task: gitversion/setup@0 +- task: gitversion/setup@1 displayName: 'Install GitVersion' inputs: versionSpec: '5.x' -- task: gitversion/execute@0 +- task: gitversion/execute@1 displayName: 'Determine Version' inputs: updateAssemblyInfo: true From 34d81aeab091c2c47a63d3354e421794bcfe83e0 Mon Sep 17 00:00:00 2001 From: Mark Carrington <31017244+MarkMpn@users.noreply.github.com> Date: Wed, 3 Jul 2024 20:52:58 +0100 Subject: [PATCH 21/25] Updated post-build events --- ...rkMpn.Sql4Cds.DebugVisualizer.DebuggerSide.csproj | 1 + MarkMpn.Sql4Cds.Engine/MarkMpn.Sql4Cds.Engine.csproj | 9 +++++++++ MarkMpn.Sql4Cds.Export/MarkMpn.Sql4Cds.Export.csproj | 5 +++++ MarkMpn.Sql4Cds.XTB/MarkMpn.Sql4Cds.XTB.csproj | 12 ++++-------- MarkMpn.Sql4Cds/MarkMpn.Sql4Cds.csproj | 2 +- 5 files changed, 20 insertions(+), 9 deletions(-) diff --git a/MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide/MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide.csproj b/MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide/MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide.csproj index eed07fee..42d72b88 100644 --- a/MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide/MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide.csproj +++ b/MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide/MarkMpn.Sql4Cds.DebugVisualizer.DebuggerSide.csproj @@ -43,5 +43,6 @@ + diff --git a/MarkMpn.Sql4Cds.Engine/MarkMpn.Sql4Cds.Engine.csproj b/MarkMpn.Sql4Cds.Engine/MarkMpn.Sql4Cds.Engine.csproj index 021569bc..73e366a1 100644 --- a/MarkMpn.Sql4Cds.Engine/MarkMpn.Sql4Cds.Engine.csproj +++ b/MarkMpn.Sql4Cds.Engine/MarkMpn.Sql4Cds.Engine.csproj @@ -39,4 +39,13 @@ + + + + + + + + + diff --git a/MarkMpn.Sql4Cds.Export/MarkMpn.Sql4Cds.Export.csproj b/MarkMpn.Sql4Cds.Export/MarkMpn.Sql4Cds.Export.csproj index ca9482f7..b143ca34 100644 --- a/MarkMpn.Sql4Cds.Export/MarkMpn.Sql4Cds.Export.csproj +++ b/MarkMpn.Sql4Cds.Export/MarkMpn.Sql4Cds.Export.csproj @@ -19,4 +19,9 @@ + + + + + diff --git a/MarkMpn.Sql4Cds.XTB/MarkMpn.Sql4Cds.XTB.csproj b/MarkMpn.Sql4Cds.XTB/MarkMpn.Sql4Cds.XTB.csproj index ce0ed64f..e22c2da8 100644 --- a/MarkMpn.Sql4Cds.XTB/MarkMpn.Sql4Cds.XTB.csproj +++ b/MarkMpn.Sql4Cds.XTB/MarkMpn.Sql4Cds.XTB.csproj @@ -311,13 +311,9 @@ - copy $(TargetDir)MarkMpn.Sql4Cds.XTB.dll %25appdata%25\MscrmTools\XrmToolBox\Plugins\MarkMpn.Sql4Cds - - - copy $(TargetDir)MarkMpn.Sql4Cds.Engine.dll %25appdata%25\MscrmTools\XrmToolBox\Plugins\MarkMpn.Sql4Cds -copy $(TargetDir)MarkMpn.Sql4Cds.XTB.dll %25appdata%25\MscrmTools\XrmToolBox\Plugins\MarkMpn.Sql4Cds -copy $(TargetDir)MarkMpn.Sql4Cds.Export.dll %25appdata%25\MscrmTools\XrmToolBox\Plugins\MarkMpn.Sql4Cds -copy $(TargetDir)SkiaSharp.dll %25appdata%25\MscrmTools\XrmToolBox\Plugins\MarkMpn.Sql4Cds -copy $(TargetDir)System.Text.Encoding.CodePages.dll %25appdata%25\MscrmTools\XrmToolBox\Plugins\MarkMpn.Sql4Cds + mkdir $(AppData)\MscrmTools\XrmToolBox\Plugins\MarkMpn.Sql4Cds +copy $(TargetDir)MarkMpn.Sql4Cds.XTB.dll $(AppData)\MscrmTools\XrmToolBox\Plugins\MarkMpn.Sql4Cds +copy $(TargetDir)SkiaSharp.dll $(AppData)\MscrmTools\XrmToolBox\Plugins\MarkMpn.Sql4Cds +copy $(TargetDir)System.Text.Encoding.CodePages.dll $(AppData)\MscrmTools\XrmToolBox\Plugins\MarkMpn.Sql4Cds \ No newline at end of file diff --git a/MarkMpn.Sql4Cds/MarkMpn.Sql4Cds.csproj b/MarkMpn.Sql4Cds/MarkMpn.Sql4Cds.csproj index 2de50353..3c84beaf 100644 --- a/MarkMpn.Sql4Cds/MarkMpn.Sql4Cds.csproj +++ b/MarkMpn.Sql4Cds/MarkMpn.Sql4Cds.csproj @@ -98,7 +98,7 @@ - copy $(TargetDir)MarkMpn.Sql4Cds.dll %25appdata%25\MscrmTools\XrmToolBox\Plugins + copy $(TargetDir)MarkMpn.Sql4Cds.dll $(AppData)\MscrmTools\XrmToolBox\Plugins