diff --git a/MarkMpn.Sql4Cds.Engine.Tests/ExecutionPlanTests.cs b/MarkMpn.Sql4Cds.Engine.Tests/ExecutionPlanTests.cs index f89595d3..d493a3b2 100644 --- a/MarkMpn.Sql4Cds.Engine.Tests/ExecutionPlanTests.cs +++ b/MarkMpn.Sql4Cds.Engine.Tests/ExecutionPlanTests.cs @@ -77,10 +77,6 @@ void IQueryExecutionOptions.Progress(double? progress, string message) { } - void IQueryExecutionOptions.RetrievingNextPage() - { - } - string IQueryExecutionOptions.PrimaryDataSource => "local"; Guid IQueryExecutionOptions.UserId => Guid.NewGuid(); diff --git a/MarkMpn.Sql4Cds.Engine.Tests/OptionsWrapper.cs b/MarkMpn.Sql4Cds.Engine.Tests/OptionsWrapper.cs index 94cb3618..8ff86a19 100644 --- a/MarkMpn.Sql4Cds.Engine.Tests/OptionsWrapper.cs +++ b/MarkMpn.Sql4Cds.Engine.Tests/OptionsWrapper.cs @@ -88,10 +88,5 @@ public void Progress(double? progress, string message) { _options.Progress(progress, message); } - - public void RetrievingNextPage() - { - _options.RetrievingNextPage(); - } } } diff --git a/MarkMpn.Sql4Cds.Engine.Tests/Sql2FetchXmlTests.cs b/MarkMpn.Sql4Cds.Engine.Tests/Sql2FetchXmlTests.cs index 7df6f3e3..33119078 100644 --- a/MarkMpn.Sql4Cds.Engine.Tests/Sql2FetchXmlTests.cs +++ b/MarkMpn.Sql4Cds.Engine.Tests/Sql2FetchXmlTests.cs @@ -50,10 +50,6 @@ public class Sql2FetchXmlTests : FakeXrmEasyTestsBase, IQueryExecutionOptions bool IQueryExecutionOptions.BypassCustomPlugins => false; - void IQueryExecutionOptions.RetrievingNextPage() - { - } - string IQueryExecutionOptions.PrimaryDataSource => "local"; Guid IQueryExecutionOptions.UserId => Guid.NewGuid(); diff --git a/MarkMpn.Sql4Cds.Engine.Tests/StubOptions.cs b/MarkMpn.Sql4Cds.Engine.Tests/StubOptions.cs index 8693f490..aab69e16 100644 --- a/MarkMpn.Sql4Cds.Engine.Tests/StubOptions.cs +++ b/MarkMpn.Sql4Cds.Engine.Tests/StubOptions.cs @@ -59,10 +59,6 @@ void IQueryExecutionOptions.Progress(double? progress, string message) { } - void IQueryExecutionOptions.RetrievingNextPage() - { - } - string IQueryExecutionOptions.PrimaryDataSource => "local"; Guid IQueryExecutionOptions.UserId => Guid.NewGuid(); diff --git a/MarkMpn.Sql4Cds.Engine/Ado/CancellationTokenOptionsWrapper.cs b/MarkMpn.Sql4Cds.Engine/Ado/CancellationTokenOptionsWrapper.cs index 5c79c77f..0984ba15 100644 --- a/MarkMpn.Sql4Cds.Engine/Ado/CancellationTokenOptionsWrapper.cs +++ b/MarkMpn.Sql4Cds.Engine/Ado/CancellationTokenOptionsWrapper.cs @@ -73,10 +73,5 @@ public void Progress(double? progress, string message) { _options.Progress(progress, message); } - - public void RetrievingNextPage() - { - _options.RetrievingNextPage(); - } } } diff --git a/MarkMpn.Sql4Cds.Engine/Ado/ChangeDatabaseOptionsWrapper.cs b/MarkMpn.Sql4Cds.Engine/Ado/ChangeDatabaseOptionsWrapper.cs index 21db84b7..d7e76e70 100644 --- a/MarkMpn.Sql4Cds.Engine/Ado/ChangeDatabaseOptionsWrapper.cs +++ b/MarkMpn.Sql4Cds.Engine/Ado/ChangeDatabaseOptionsWrapper.cs @@ -9,72 +9,122 @@ namespace MarkMpn.Sql4Cds.Engine { class ChangeDatabaseOptionsWrapper : IQueryExecutionOptions { + private readonly Sql4CdsConnection _connection; private readonly IQueryExecutionOptions _options; - - public ChangeDatabaseOptionsWrapper(IQueryExecutionOptions options) + + public ChangeDatabaseOptionsWrapper(Sql4CdsConnection connection, IQueryExecutionOptions options) { + _connection = connection; _options = options; + PrimaryDataSource = options.PrimaryDataSource; + UseTDSEndpoint = options.UseTDSEndpoint; + BlockUpdateWithoutWhere = options.BlockUpdateWithoutWhere; + BlockDeleteWithoutWhere = options.BlockDeleteWithoutWhere; + UseBulkDelete = options.UseBulkDelete; + BatchSize = options.BatchSize; + UseRetrieveTotalRecordCount = options.UseRetrieveTotalRecordCount; + MaxDegreeOfParallelism = options.MaxDegreeOfParallelism; + ColumnComparisonAvailable = options.ColumnComparisonAvailable; + UseLocalTimeZone = options.UseLocalTimeZone; + BypassCustomPlugins = options.BypassCustomPlugins; + QuotedIdentifiers = options.QuotedIdentifiers; } public CancellationToken CancellationToken => _options.CancellationToken; - public bool BlockUpdateWithoutWhere => _options.BlockUpdateWithoutWhere; + public bool BlockUpdateWithoutWhere { get; set; } - public bool BlockDeleteWithoutWhere => _options.BlockDeleteWithoutWhere; + public bool BlockDeleteWithoutWhere { get; set; } - public bool UseBulkDelete => _options.UseBulkDelete; + public bool UseBulkDelete { get; set; } - public int BatchSize => _options.BatchSize; + public int BatchSize { get; set; } - public bool UseTDSEndpoint => _options.UseTDSEndpoint; + public bool UseTDSEndpoint { get; set; } - public bool UseRetrieveTotalRecordCount => _options.UseRetrieveTotalRecordCount; + public bool UseRetrieveTotalRecordCount { get; set; } - public int MaxDegreeOfParallelism => _options.MaxDegreeOfParallelism; + public int MaxDegreeOfParallelism { get; set; } - public bool ColumnComparisonAvailable => _options.ColumnComparisonAvailable; + public bool ColumnComparisonAvailable { get; } - public bool UseLocalTimeZone => _options.UseLocalTimeZone; + public bool UseLocalTimeZone { get; set; } public List JoinOperatorsAvailable => _options.JoinOperatorsAvailable; - public bool BypassCustomPlugins => _options.BypassCustomPlugins; + public bool BypassCustomPlugins { get; set; } - public string PrimaryDataSource { get; set; } + public string PrimaryDataSource { get; set; } // TODO: Update UserId when changing data source public Guid UserId => _options.UserId; - public bool QuotedIdentifiers => _options.QuotedIdentifiers; + public bool QuotedIdentifiers { get; set; } public bool ConfirmDelete(int count, EntityMetadata meta) { - return _options.ConfirmDelete(count, meta); + var cancelled = !_options.ConfirmDelete(count, meta); + + if (!cancelled) + { + var args = new ConfirmDmlStatementEventArgs(count, meta); + _connection.OnPreDelete(args); + + cancelled = args.Cancel; + } + + return !cancelled; } public bool ConfirmInsert(int count, EntityMetadata meta) { - return _options.ConfirmInsert(count, meta); + var cancelled = !_options.ConfirmInsert(count, meta); + + if (!cancelled) + { + var args = new ConfirmDmlStatementEventArgs(count, meta); + _connection.OnPreInsert(args); + + cancelled = args.Cancel; + } + + return !cancelled; } public bool ConfirmUpdate(int count, EntityMetadata meta) { - return _options.ConfirmUpdate(count, meta); + var cancelled = !_options.ConfirmUpdate(count, meta); + + if (!cancelled) + { + var args = new ConfirmDmlStatementEventArgs(count, meta); + _connection.OnPreUpdate(args); + + cancelled = args.Cancel; + } + + return !cancelled; } public bool ContinueRetrieve(int count) { - return _options.ContinueRetrieve(count); + var cancelled = !_options.ContinueRetrieve(count); + + if (!cancelled) + { + var args = new ConfirmRetrieveEventArgs(count); + _connection.OnPreRetrieve(args); + + cancelled = args.Cancel; + } + + return !cancelled; } public void Progress(double? progress, string message) { _options.Progress(progress, message); - } - - public void RetrievingNextPage() - { - _options.RetrievingNextPage(); + _connection.OnProgress(new ProgressEventArgs(progress, message)); } } } diff --git a/MarkMpn.Sql4Cds.Engine/Ado/ConfirmDmlStatementEventArgs.cs b/MarkMpn.Sql4Cds.Engine/Ado/ConfirmDmlStatementEventArgs.cs new file mode 100644 index 00000000..97d4346d --- /dev/null +++ b/MarkMpn.Sql4Cds.Engine/Ado/ConfirmDmlStatementEventArgs.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Text; +using Microsoft.Xrm.Sdk.Metadata; + +namespace MarkMpn.Sql4Cds.Engine +{ + /// + /// Contains information about a DML statement (INSERT/UPDATE/DELETE) that is about to be executed + /// + public class ConfirmDmlStatementEventArgs : CancelEventArgs + { + /// + /// Creates a new + /// + /// The number of records that will be affected + /// The metadata of the entity that will be affected + internal ConfirmDmlStatementEventArgs(int count, EntityMetadata metadata) + { + Count = count; + Metadata = metadata; + } + + /// + /// Returns the number of records that will be affected by the operation + /// + public int Count { get; } + + /// + /// Returns the metadata of the entity that will be affected + /// + public EntityMetadata Metadata { get; } + } +} diff --git a/MarkMpn.Sql4Cds.Engine/Ado/ConfirmRetrieveEventArgs.cs b/MarkMpn.Sql4Cds.Engine/Ado/ConfirmRetrieveEventArgs.cs new file mode 100644 index 00000000..a2ba8360 --- /dev/null +++ b/MarkMpn.Sql4Cds.Engine/Ado/ConfirmRetrieveEventArgs.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Text; + +namespace MarkMpn.Sql4Cds.Engine +{ + /// + /// Contains information about a page of data that is about to be retrieved + /// + public class ConfirmRetrieveEventArgs : CancelEventArgs + { + /// + /// Creates a new + /// + /// The number of records retrieved so far + public ConfirmRetrieveEventArgs(int count) + { + Count = count; + } + + /// + /// Returns the number of records retrieved so far + /// + public int Count { get; } + } +} diff --git a/MarkMpn.Sql4Cds.Engine/Ado/ProgressEventArgs.cs b/MarkMpn.Sql4Cds.Engine/Ado/ProgressEventArgs.cs new file mode 100644 index 00000000..c112de28 --- /dev/null +++ b/MarkMpn.Sql4Cds.Engine/Ado/ProgressEventArgs.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace MarkMpn.Sql4Cds.Engine +{ + /// + /// Holds information about the progress of a query + /// + public class ProgressEventArgs + { + /// + /// Creates a new + /// + /// The percentage of the operation that has completed so far + /// A human-readable status message to display + internal ProgressEventArgs(double? progress, string message) + { + Progress = progress; + Message = message; + } + + /// + /// The percentage of the operation that has completed so far + /// + /// + /// This is expressed as a number between 0 and 1. + /// + public double? Progress { get; } + + /// + /// A human-readable status message to display + /// + public string Message { get; } + } +} diff --git a/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsConnection.cs b/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsConnection.cs index 19b108ff..f720b5e4 100644 --- a/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsConnection.cs +++ b/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsConnection.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Data; using System.Data.Common; using System.Linq; @@ -56,7 +57,7 @@ public Sql4CdsConnection(IReadOnlyList dataSources) var options = new DefaultQueryExecutionOptions(dataSources[0], CancellationToken.None); _dataSources = dataSources.ToDictionary(ds => ds.Name, StringComparer.OrdinalIgnoreCase); - _options = new ChangeDatabaseOptionsWrapper(options); + _options = new ChangeDatabaseOptionsWrapper(this, options); } private static IOrganizationService Connect(string connectionString) @@ -115,8 +116,169 @@ internal void OnInfoMessage(IRootExecutionPlanNode node, string message) internal IQueryExecutionOptions Options => _options; + /// + /// Indicates if values should be returned as s from the data reader + /// public bool ReturnEntityReferenceAsGuid { get; set; } + /// + /// Indicates if the Dataverse TDS Endpoint should be used for query execution where possible + /// + public bool UseTDSEndpoint + { + get => _options.UseTDSEndpoint; + set => _options.UseTDSEndpoint = value; + } + + /// + /// Indicates if an UPDATE statement cannot be executed unless it has a WHERE clause + /// + public bool BlockUpdateWithoutWhere + { + get => _options.BlockUpdateWithoutWhere; + set => _options.BlockUpdateWithoutWhere = value; + } + + /// + /// Indicates if a DELETE statement cannot be execyted unless it has a WHERE clause + /// + public bool BlockDeleteWithoutWhere + { + get => _options.BlockDeleteWithoutWhere; + set => _options.BlockDeleteWithoutWhere = value; + } + + /// + /// Indicates if DELETE queries should be executed with a bulk delete job + /// + public bool UseBulkDelete + { + get => _options.UseBulkDelete; + set => _options.UseBulkDelete = value; + } + + /// + /// The number of records that should be inserted, updated or deleted in a single batch + /// + public int BatchSize + { + get => _options.BatchSize; + set => _options.BatchSize = value; + } + + /// + /// Indicates if a should be used for simple SELECT count(*) FROM table queries + /// + public bool UseRetrieveTotalRecordCount + { + get => _options.UseRetrieveTotalRecordCount; + set => _options.UseRetrieveTotalRecordCount = value; + } + + /// + /// The maximum number of worker threads to use to execute DML queries + /// + public int MaxDegreeOfParallelism + { + get => _options.MaxDegreeOfParallelism; + set => _options.MaxDegreeOfParallelism = value; + } + + /// + /// Indicates if date/time values should be interpreted in the local timezone or in UTC + /// + public bool UseLocalTimeZone + { + get => _options.UseLocalTimeZone; + set => _options.UseLocalTimeZone = value; + } + + /// + /// Indicates if plugins should be bypassed when executing DML operations + /// + public bool BypassCustomPlugins + { + get => _options.BypassCustomPlugins; + set => _options.BypassCustomPlugins = value; + } + + /// + /// Returns or sets a value indicating if SQL will be parsed using quoted identifiers + /// + public bool QuotedIdentifiers + { + get => _options.QuotedIdentifiers; + set => _options.QuotedIdentifiers = value; + } + + /// + /// Triggered before one or more records are about to be deleted + /// + /// + /// Set the property to true to prevent the deletion + /// + public event EventHandler PreDelete; + + internal void OnPreDelete(ConfirmDmlStatementEventArgs args) + { + PreDelete?.Invoke(this, args); + } + + /// + /// Triggered before one or more records are about to be inserted + /// + /// + /// Set the property to true to prevent the insertion + /// + public event EventHandler PreInsert; + + internal void OnPreInsert(ConfirmDmlStatementEventArgs args) + { + PreInsert?.Invoke(this, args); + } + + /// + /// Triggered before one or more records are about to be updated + /// + /// + /// Set the property to true to prevent the update + /// + public event EventHandler PreUpdate; + + internal void OnPreUpdate(ConfirmDmlStatementEventArgs args) + { + PreUpdate?.Invoke(this, args); + } + + /// + /// Triggered before a page of data is about to be retrieved + /// + /// + /// Set the property to true to prevent the retrieval + /// + public event EventHandler PreRetrieve; + + internal void OnPreRetrieve(ConfirmRetrieveEventArgs args) + { + PreRetrieve?.Invoke(this, args); + } + + /// + /// Triggered when there is some progress that can be reported to the user + /// + /// + /// This event does not signify that an operation has completed successfully. It can be triggered multiple times + /// for the same operation. Use to receive a notification that a + /// statement has completed successfully, or to receive messages that should be shown + /// as part of the main output of a query. + /// + public event EventHandler Progress; + + internal void OnProgress(ProgressEventArgs args) + { + Progress?.Invoke(this, args); + } + public override string ConnectionString { get; set; } public override string Database => _options.PrimaryDataSource; diff --git a/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsDataReader.cs b/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsDataReader.cs index f0aa7227..a958251b 100644 --- a/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsDataReader.cs +++ b/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsDataReader.cs @@ -142,6 +142,11 @@ private void Execute(IRootExecutionPlanNode[] plan, Dictionary ExecuteInternal(IDictionary ExecuteInternal(IDictionary ExecuteInternal(IDictionary bool BypassCustomPlugins { get; } - /// - /// A notification that the query is about to retrieve another page of data - /// - void RetrievingNextPage(); - /// /// Returns the name of the primary data source the query is being executed in /// diff --git a/MarkMpn.Sql4Cds.Engine/MarkMpn.Sql4Cds.Engine.projitems b/MarkMpn.Sql4Cds.Engine/MarkMpn.Sql4Cds.Engine.projitems index bdb91c6d..268c73c2 100644 --- a/MarkMpn.Sql4Cds.Engine/MarkMpn.Sql4Cds.Engine.projitems +++ b/MarkMpn.Sql4Cds.Engine/MarkMpn.Sql4Cds.Engine.projitems @@ -11,9 +11,12 @@ + + + diff --git a/MarkMpn.Sql4Cds.SSMS/DmlExecute.cs b/MarkMpn.Sql4Cds.SSMS/DmlExecute.cs index 0ea6072c..98442f30 100644 --- a/MarkMpn.Sql4Cds.SSMS/DmlExecute.cs +++ b/MarkMpn.Sql4Cds.SSMS/DmlExecute.cs @@ -72,12 +72,13 @@ private void OnExecuteQuery(string Guid, int ID, object CustomIn, object CustomO return; // Store the options being used for these queries so we can cancel them later - var options = new QueryExecutionOptions(sqlScriptEditorControl, Package.Settings, true); var dataSource = GetDataSource(); - using (var con = new Sql4CdsConnection(new[] { dataSource }, options)) + using (var con = new Sql4CdsConnection(new[] { dataSource })) using (var cmd = con.CreateCommand()) { + var options = new QueryExecutionOptions(sqlScriptEditorControl, Package.Settings, true, cmd); + options.ApplySettings(con); cmd.CommandText = sql; try @@ -164,7 +165,7 @@ private void OnExecuteQuery(string Guid, int ID, object CustomIn, object CustomO resultFlag |= 2; // Failure } - if (options.CancellationToken.IsCancellationRequested) + if (options.IsCancelled) resultFlag = 4; // Cancel await Package.JoinableTaskFactory.SwitchToMainThreadAsync(); diff --git a/MarkMpn.Sql4Cds.SSMS/QueryExecutionOptions.cs b/MarkMpn.Sql4Cds.SSMS/QueryExecutionOptions.cs index b026f966..f0d832c2 100644 --- a/MarkMpn.Sql4Cds.SSMS/QueryExecutionOptions.cs +++ b/MarkMpn.Sql4Cds.SSMS/QueryExecutionOptions.cs @@ -9,86 +9,85 @@ namespace MarkMpn.Sql4Cds.SSMS { - internal class QueryExecutionOptions : IQueryExecutionOptions + class QueryExecutionOptions { private readonly SqlScriptEditorControlWrapper _sqlScriptEditorControl; private readonly OptionsPage _options; - private readonly CancellationTokenSource _cts; + private readonly bool _useTds; + private readonly Sql4CdsCommand _cmd; - public QueryExecutionOptions(SqlScriptEditorControlWrapper sqlScriptEditorControl, OptionsPage options, bool useTds) + public QueryExecutionOptions(SqlScriptEditorControlWrapper sqlScriptEditorControl, OptionsPage options, bool useTds, Sql4CdsCommand cmd) { _sqlScriptEditorControl = sqlScriptEditorControl; _options = options; - _cts = new CancellationTokenSource(); - UseTDSEndpoint = useTds; + _useTds = useTds; + _cmd = cmd; } - public bool QuotedIdentifiers => _sqlScriptEditorControl.QuotedIdentifiers; - - public CancellationToken CancellationToken => _cts.Token; - - public bool BlockUpdateWithoutWhere => _options.BlockUpdateWithoutWhere; - - public bool BlockDeleteWithoutWhere => _options.BlockDeleteWithoutWhere; - - public bool UseBulkDelete => false; - - public int BatchSize => _options.BatchSize; - - public bool UseTDSEndpoint { get; } - - public bool UseRetrieveTotalRecordCount => false; - - public int MaxDegreeOfParallelism => _options.MaxDegreeOfParallelism; - - public bool ColumnComparisonAvailable => true; - - public bool UseLocalTimeZone => false; - - public List JoinOperatorsAvailable => new List(); - - public bool BypassCustomPlugins => _options.BypassCustomPlugins; - - public string PrimaryDataSource => new SqlConnectionStringBuilder(_sqlScriptEditorControl.ConnectionString).DataSource.Split('.')[0]; + public void ApplySettings(Sql4CdsConnection con) + { + con.QuotedIdentifiers = _sqlScriptEditorControl.QuotedIdentifiers; + con.BlockUpdateWithoutWhere = _options.BlockUpdateWithoutWhere; + con.BlockDeleteWithoutWhere = _options.BlockDeleteWithoutWhere; + con.UseBulkDelete = false; + con.BatchSize = _options.BatchSize; + con.UseTDSEndpoint = _useTds; + con.UseRetrieveTotalRecordCount = false; + con.MaxDegreeOfParallelism = _options.MaxDegreeOfParallelism; + con.UseLocalTimeZone = false; + con.BypassCustomPlugins = _options.BypassCustomPlugins; + + con.PreInsert += ConfirmInsert; + con.PreDelete += ConfirmDelete; + con.PreUpdate += ConfirmUpdate; + con.Progress += Progress; + } - public Guid UserId => Guid.Empty; + private void ConfirmInsert(object sender, ConfirmDmlStatementEventArgs e) + { + ConfirmInsert(e.Count, e.Metadata); + } - public bool ConfirmInsert(int count, EntityMetadata meta) + private void ConfirmInsert(int count, EntityMetadata meta) { if (count == 1) _sqlScriptEditorControl.Results.AddStringToMessages($"Inserting 1 {meta.DisplayName?.UserLocalizedLabel?.Label ?? meta.LogicalName}...\r\n"); else _sqlScriptEditorControl.Results.AddStringToMessages($"Inserting {count:N0} {meta.DisplayCollectionName?.UserLocalizedLabel?.Label ?? meta.LogicalCollectionName ?? meta.LogicalName}...\r\n"); + } - return true; + private void ConfirmDelete(object sender, ConfirmDmlStatementEventArgs e) + { + ConfirmDelete(e.Count, e.Metadata); } - public bool ConfirmDelete(int count, EntityMetadata meta) + private void ConfirmDelete(int count, EntityMetadata meta) { if (count == 1) _sqlScriptEditorControl.Results.AddStringToMessages($"Deleting 1 {meta.DisplayName?.UserLocalizedLabel?.Label ?? meta.LogicalName}...\r\n"); else _sqlScriptEditorControl.Results.AddStringToMessages($"Deleting {count:N0} {meta.DisplayCollectionName?.UserLocalizedLabel?.Label ?? meta.LogicalCollectionName ?? meta.LogicalName}...\r\n"); + } - return true; + private void ConfirmUpdate(object sender, ConfirmDmlStatementEventArgs e) + { + ConfirmUpdate(e.Count, e.Metadata); } - public bool ConfirmUpdate(int count, EntityMetadata meta) + private void ConfirmUpdate(int count, EntityMetadata meta) { if (count == 1) _sqlScriptEditorControl.Results.AddStringToMessages($"Updating 1 {meta.DisplayName?.UserLocalizedLabel?.Label ?? meta.LogicalName}...\r\n"); else _sqlScriptEditorControl.Results.AddStringToMessages($"Updating {count:N0} {meta.DisplayCollectionName?.UserLocalizedLabel?.Label ?? meta.LogicalCollectionName ?? meta.LogicalName}...\r\n"); - - return true; } - public bool ContinueRetrieve(int count) + private void Progress(object sender, ProgressEventArgs e) { - return true; + Progress(e.Progress, e.Message); } - public void Progress(double? progress, string message) + private void Progress(double? progress, string message) { if (progress != null) _sqlScriptEditorControl.Results.OnQueryProgressUpdateEstimate(progress.Value); @@ -98,13 +97,12 @@ public void Progress(double? progress, string message) public void Cancel() { + IsCancelled = true; _sqlScriptEditorControl.Cancelling(); Task.ContinueWith(t => _sqlScriptEditorControl.DoCancelExec()); - _cts.Cancel(); + _cmd.Cancel(); } - public void RetrievingNextPage() - { - } + public bool IsCancelled { get; private set; } } } \ No newline at end of file diff --git a/MarkMpn.Sql4Cds.SSMS/Sql2FetchXmlCommand.cs b/MarkMpn.Sql4Cds.SSMS/Sql2FetchXmlCommand.cs index a2e84fc0..3ed823ae 100644 --- a/MarkMpn.Sql4Cds.SSMS/Sql2FetchXmlCommand.cs +++ b/MarkMpn.Sql4Cds.SSMS/Sql2FetchXmlCommand.cs @@ -97,14 +97,14 @@ private void Execute(object sender, EventArgs e) var scriptFactory = new ScriptFactoryWrapper(ServiceCache.ScriptFactory); var sqlScriptEditorControl = scriptFactory.GetCurrentlyActiveFrameDocView(ServiceCache.VSMonitorSelection, false, out _); - var options = new QueryExecutionOptions(sqlScriptEditorControl, Package.Settings, false); var dataSource = GetDataSource(); try { - using (var con = new Sql4CdsConnection(new[] { dataSource }, options)) + using (var con = new Sql4CdsConnection(new[] { dataSource })) using (var cmd = con.CreateCommand()) { + new QueryExecutionOptions(sqlScriptEditorControl, Package.Settings, false, cmd).ApplySettings(con); cmd.CommandText = sql; var queries = cmd.GeneratePlan(false); diff --git a/MarkMpn.Sql4Cds/QueryExecutionOptions.cs b/MarkMpn.Sql4Cds/QueryExecutionOptions.cs index 61b21912..d313db73 100644 --- a/MarkMpn.Sql4Cds/QueryExecutionOptions.cs +++ b/MarkMpn.Sql4Cds/QueryExecutionOptions.cs @@ -17,51 +17,49 @@ namespace MarkMpn.Sql4Cds { class QueryExecutionOptions { - private readonly ConnectionDetail _con; - private readonly IOrganizationService _org; - private readonly BackgroundWorker _worker; private readonly Control _host; - private readonly List _joinOperators; + private readonly BackgroundWorker _worker; private int _retrievedPages; - private Guid? _userId; - public QueryExecutionOptions(ConnectionDetail con, IOrganizationService org, BackgroundWorker worker, Control host, CancellationToken cancellationToken) + public QueryExecutionOptions(Control host, BackgroundWorker worker) { - _con = con; - _org = org; - _worker = worker; _host = host; - _joinOperators = new List - { - JoinOperator.Inner, - JoinOperator.LeftOuter - }; - - if (new Version(con.OrganizationVersion) >= new Version("9.1.0.17461")) - { - // First documented in SDK Version 9.0.2.25: Updated for 9.1.0.17461 CDS release - _joinOperators.Add(JoinOperator.Any); - _joinOperators.Add(JoinOperator.Exists); - } - - CancellationToken = cancellationToken; - UseTDSEndpoint = Settings.Instance.UseTSQLEndpoint; + _worker = worker; } - public CancellationToken CancellationToken { get; } - - public bool BlockUpdateWithoutWhere => Settings.Instance.BlockUpdateWithoutWhere; - - public bool BlockDeleteWithoutWhere => Settings.Instance.BlockDeleteWithoutWhere; + public void ApplySettings(Sql4CdsConnection con, Sql4CdsCommand cmd, bool execute) + { + con.BlockDeleteWithoutWhere = Settings.Instance.BlockDeleteWithoutWhere; + con.BlockUpdateWithoutWhere = Settings.Instance.BlockUpdateWithoutWhere; + con.UseBulkDelete = Settings.Instance.UseBulkDelete; + con.BatchSize = Settings.Instance.BatchSize; + con.UseTDSEndpoint = Settings.Instance.UseTSQLEndpoint && execute; + con.UseRetrieveTotalRecordCount = Settings.Instance.UseRetrieveTotalRecordCount; + con.MaxDegreeOfParallelism = Settings.Instance.MaxDegreeOfPaallelism; + con.UseLocalTimeZone = Settings.Instance.ShowLocalTimes; + con.BypassCustomPlugins = Settings.Instance.BypassCustomPlugins; + con.QuotedIdentifiers = Settings.Instance.QuotedIdentifiers; + + con.PreInsert += ConfirmInsert; + con.PreUpdate += ConfirmUpdate; + con.PreDelete += ConfirmDelete; + con.PreRetrieve += ConfirmRetrieve; + con.Progress += Progress; + + cmd.StatementCompleted += StatementCompleted; + } - public bool UseBulkDelete => Settings.Instance.UseBulkDelete; + private void ConfirmInsert(object sender, ConfirmDmlStatementEventArgs e) + { + e.Cancel |= ConfirmInsert((Sql4CdsConnection)sender, e.Count, e.Metadata); + } - public bool ConfirmInsert(int count, EntityMetadata meta) + private bool ConfirmInsert(Sql4CdsConnection con, int count, EntityMetadata meta) { - if (count > Settings.Instance.InsertWarnThreshold || BypassCustomPlugins) + if (count > Settings.Instance.InsertWarnThreshold || con.BypassCustomPlugins) { var msg = $"Insert will affect {count:N0} {GetDisplayName(count, meta)}."; - if (BypassCustomPlugins) + if (con.BypassCustomPlugins) msg += "\r\n\r\nThis operation will bypass any custom plugins."; var result = MessageBox.Show(_host, msg + "\r\n\r\nDo you want to proceed?", "Confirm", MessageBoxButtons.YesNo, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2); @@ -73,12 +71,17 @@ public bool ConfirmInsert(int count, EntityMetadata meta) return true; } - public bool ConfirmUpdate(int count, EntityMetadata meta) + private void ConfirmUpdate(object sender, ConfirmDmlStatementEventArgs e) { - if (count > Settings.Instance.UpdateWarnThreshold || BypassCustomPlugins) + e.Cancel |= ConfirmUpdate((Sql4CdsConnection)sender, e.Count, e.Metadata); + } + + private bool ConfirmUpdate(Sql4CdsConnection con, int count, EntityMetadata meta) + { + if (count > Settings.Instance.UpdateWarnThreshold || con.BypassCustomPlugins) { var msg = $"Update will affect {count:N0} {GetDisplayName(count, meta)}."; - if (BypassCustomPlugins) + if (con.BypassCustomPlugins) msg += "\r\n\r\nThis operation will bypass any custom plugins."; var result = MessageBox.Show(_host, msg + "\r\n\r\nDo you want to proceed?", "Confirm", MessageBoxButtons.YesNo, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2); @@ -90,12 +93,17 @@ public bool ConfirmUpdate(int count, EntityMetadata meta) return true; } - public bool ConfirmDelete(int count, EntityMetadata meta) + private void ConfirmDelete(object sender, ConfirmDmlStatementEventArgs e) { - if (count > Settings.Instance.DeleteWarnThreshold || BypassCustomPlugins) + e.Cancel |= ConfirmDelete((Sql4CdsConnection)sender, e.Count, e.Metadata); + } + + private bool ConfirmDelete(Sql4CdsConnection con, int count, EntityMetadata meta) + { + if (count > Settings.Instance.DeleteWarnThreshold || con.BypassCustomPlugins) { var msg = $"Delete will affect {count:N0} {GetDisplayName(count, meta)}."; - if (BypassCustomPlugins) + if (con.BypassCustomPlugins) msg += "\r\n\r\nThis operation will bypass any custom plugins."; var result = MessageBox.Show(_host, msg + "\r\n\r\nDo you want to proceed?", "Confirm", MessageBoxButtons.YesNo, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2); @@ -117,33 +125,20 @@ private string GetDisplayName(int count, EntityMetadata meta) meta.LogicalName; } - public bool ContinueRetrieve(int count) + private void ConfirmRetrieve(object sender, ConfirmRetrieveEventArgs e) { - return Settings.Instance.SelectLimit == 0 || Settings.Instance.SelectLimit > count; + e.Cancel |= ContinueRetrieve(e.Count); + + if (!e.Cancel) + RetrievingNextPage(); } - public void Progress(double? progress, string message) + private bool ContinueRetrieve(int count) { - _worker.ReportProgress(progress == null ? -1 : (int) (progress * 100), message); + return Settings.Instance.SelectLimit == 0 || Settings.Instance.SelectLimit > count; } - public int BatchSize => Settings.Instance.BatchSize; - - public bool UseTDSEndpoint { get; set; } - - public bool UseRetrieveTotalRecordCount => Settings.Instance.UseRetrieveTotalRecordCount; - - public int MaxDegreeOfParallelism => Settings.Instance.MaxDegreeOfPaallelism; - - public bool ColumnComparisonAvailable => new Version(_con.OrganizationVersion) >= new Version("9.1.0.19251"); - - public bool UseLocalTimeZone => Settings.Instance.ShowLocalTimes; - - public List JoinOperatorsAvailable => _joinOperators; - - public bool BypassCustomPlugins => Settings.Instance.BypassCustomPlugins; - - public void RetrievingNextPage() + private void RetrievingNextPage() { _retrievedPages++; @@ -151,29 +146,19 @@ public void RetrievingNextPage() throw new QueryExecutionException($"Hit maximum retrieval limit. This limit is in place to protect against excessive API requests. Try restricting the data to retrieve with WHERE clauses or eliminating subqueries.\r\nYour limit of {Settings.Instance.MaxRetrievesPerQuery:N0} retrievals per query can be modified in Settings."); } - public string PrimaryDataSource => _con.ConnectionName; - - public Guid UserId + private void StatementCompleted(object sender, StatementCompletedEventArgs e) { - get - { - if (_userId != null) - return _userId.Value; - - if (_org is CrmServiceClient svc && svc.CallerId != Guid.Empty) - _userId = svc.CallerId; - else - _userId = ((WhoAmIResponse)_org.Execute(new WhoAmIRequest())).UserId; - - return _userId.Value; - } + _retrievedPages = 0; } - public void SyncUserId() + private void Progress(object sender, ProgressEventArgs e) { - _userId = null; + Progress(e.Progress, e.Message); } - public bool QuotedIdentifiers => Settings.Instance.QuotedIdentifiers; + private void Progress(double? progress, string message) + { + _worker.ReportProgress(progress == null ? -1 : (int)(progress * 100), message); + } } } diff --git a/MarkMpn.Sql4Cds/SqlQueryControl.cs b/MarkMpn.Sql4Cds/SqlQueryControl.cs index e5d98151..a0103362 100644 --- a/MarkMpn.Sql4Cds/SqlQueryControl.cs +++ b/MarkMpn.Sql4Cds/SqlQueryControl.cs @@ -857,13 +857,11 @@ private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e) backgroundWorker.ReportProgress(0, "Executing query..."); - var options = new QueryExecutionOptions(_con, DataSources[_con.ConnectionName].Connection, backgroundWorker, this, Cancellable ? _cts.Token : CancellationToken.None); - if (!args.Execute) - options.UseTDSEndpoint = false; - using (var con = new Sql4CdsConnection(DataSources.Values.ToList())) using (var cmd = con.CreateCommand()) { + new QueryExecutionOptions(this, backgroundWorker).ApplySettings(con, cmd, args.Execute); + cmd.CommandText = args.Sql; if (args.Execute) @@ -880,10 +878,7 @@ private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e) Execute(() => ShowResult(stmt.Statement, args, null, null, null)); if (stmt.Statement is IImpersonateRevertExecutionPlanNode) - { - options.SyncUserId(); Execute(() => SyncUsername()); - } }; using (var reader = (ISql4CdsDataReader) cmd.ExecuteReader())