From 1066d34e5be62f4885df84d037ba04f9a01869eb Mon Sep 17 00:00:00 2001 From: Lau Date: Sun, 3 May 2020 18:03:53 +0200 Subject: [PATCH] Support multi-statement commands again and correctly handle parameters. --- src/System.Data.Jet/JetCommand.cs | 190 +++++++++++++++++++++++------- 1 file changed, 148 insertions(+), 42 deletions(-) diff --git a/src/System.Data.Jet/JetCommand.cs b/src/System.Data.Jet/JetCommand.cs index 85b780a..bda0cd4 100644 --- a/src/System.Data.Jet/JetCommand.cs +++ b/src/System.Data.Jet/JetCommand.cs @@ -30,7 +30,8 @@ namespace System.Data.Jet #endif _connection = source._connection; _transaction = source._transaction; - InnerCommand = source.InnerCommand; + + InnerCommand = (DbCommand) ((ICloneable) source.InnerCommand).Clone(); } /// @@ -63,7 +64,7 @@ namespace System.Data.Jet #endif } - internal DbCommand InnerCommand { get; } + protected DbCommand InnerCommand { get; } /// /// Attempts to Cancels the command execution @@ -172,17 +173,31 @@ namespace System.Data.Jet if (Connection.State != ConnectionState.Open) throw new InvalidOperationException(Messages.CannotCallMethodInThisConnectionState("ExecuteReader", ConnectionState.Open, Connection.State)); + ExpandParameters(); + + var commands = SplitCommands(); + + for (var i = 0; i < commands.Count - 1; i++) + { + commands[i] + .ExecuteNonQueryCore(); + } + + return commands[commands.Count - 1] + .ExecuteDbDataReaderCore(behavior); + } + + protected virtual DbDataReader ExecuteDbDataReaderCore(CommandBehavior behavior) + { InnerCommand.Connection = _connection.InnerConnection; // OLE DB forces us to use an existing active transaction, if one is available. InnerCommand.Transaction = _transaction?.WrappedTransaction ?? _connection.ActiveTransaction?.WrappedTransaction; - ExpandParameters(); - LogHelper.ShowCommandText("ExecuteDbDataReader", InnerCommand); if (JetStoreSchemaDefinitionRetrieve.TryGetDataReaderFromShowCommand(InnerCommand, _connection.JetFactory.InnerFactory, out var dataReader)) - // Retrieve of store schema definition + // Retrieve from store schema definition. return dataReader; if (InnerCommand.CommandType != CommandType.Text) @@ -204,22 +219,6 @@ namespace System.Data.Jet return dataReader; } - private DbDataReader TryGetDataReaderForSelectRowCount(string commandText) - { - if (_selectRowCountRegularExpression.Match(commandText).Success) - { - if (_rowCount == null) - throw new InvalidOperationException("Invalid " + commandText + ". Run a DataReader before."); - - var dataTable = new DataTable("Rowcount"); - dataTable.Columns.Add("ROWCOUNT", typeof(int)); - dataTable.Rows.Add(_rowCount.Value); - return new DataTableReader(dataTable); - } - - return null; - } - /// /// Executes the non query. /// @@ -228,13 +227,23 @@ namespace System.Data.Jet { if (Connection == null) throw new InvalidOperationException(Messages.PropertyNotInitialized(nameof(Connection))); - + ExpandParameters(); + return SplitCommands() + .Aggregate(0, (_, command) => command.ExecuteNonQueryCore()); + } + + protected virtual int ExecuteNonQueryCore() + { + if (Connection == null) + throw new InvalidOperationException(Messages.PropertyNotInitialized(nameof(Connection))); + LogHelper.ShowCommandText("ExecuteNonQuery", InnerCommand); if (JetStoreDatabaseHandling.TryDatabaseOperation(this)) return 1; + if (JetRenameHandling.TryDatabaseOperation(Connection.ConnectionString, InnerCommand.CommandText)) return 1; @@ -248,13 +257,14 @@ namespace System.Data.Jet if (InnerCommand.CommandType != CommandType.Text) return InnerCommand.ExecuteNonQuery(); - + if (_selectRowCountRegularExpression.Match(InnerCommand.CommandText) .Success) { // TODO: Fix exception message. if (_rowCount == null) throw new InvalidOperationException("Invalid " + InnerCommand.CommandText + ". Run a DataReader before."); + return _rowCount.Value; } @@ -265,7 +275,7 @@ namespace System.Data.Jet return 0; InnerCommand.CommandText = newCommandText; - + InlineTopParameters(); FixParameters(); @@ -279,6 +289,28 @@ namespace System.Data.Jet /// /// public override object ExecuteScalar() + { + if (Connection == null) + throw new InvalidOperationException(Messages.PropertyNotInitialized(nameof(Connection))); + + if (Connection.State != ConnectionState.Open) + throw new InvalidOperationException(Messages.CannotCallMethodInThisConnectionState(nameof(ExecuteScalar), ConnectionState.Open, Connection.State)); + + ExpandParameters(); + + var commands = SplitCommands(); + + for (var i = 0; i < commands.Count - 1; i++) + { + commands[i] + .ExecuteNonQueryCore(); + } + + return commands[commands.Count - 1] + .ExecuteScalarCore(); + } + + protected virtual object ExecuteScalarCore() { if (Connection == null) throw new InvalidOperationException(Messages.PropertyNotInitialized(nameof(Connection))); @@ -291,13 +323,11 @@ namespace System.Data.Jet // OLE DB forces us to use an existing active transaction, if one is available. InnerCommand.Transaction = _transaction?.WrappedTransaction ?? _connection.ActiveTransaction?.WrappedTransaction; - ExpandParameters(); - LogHelper.ShowCommandText("ExecuteScalar", InnerCommand); if (JetStoreSchemaDefinitionRetrieve.TryGetDataReaderFromShowCommand(InnerCommand, _connection.JetFactory.InnerFactory, out var dataReader)) { - // Retrieve of store schema definition + // Retrieve from store schema definition. if (dataReader.HasRows) { dataReader.Read(); @@ -313,6 +343,76 @@ namespace System.Data.Jet return InnerCommand.ExecuteScalar(); } + protected virtual IReadOnlyList SplitCommands() + { + // At this point, all parameters have already been expanded. + + var parser = new JetCommandParser(CommandText); + var commandDelimiters = parser.GetStateIndices(';'); + var currentCommandStart = 0; + var usedParameterCount = 0; + var commands = new List(); + + if (commandDelimiters.Count > 0) + { + foreach (var commandDelimiter in commandDelimiters) + { + var commandText = CommandText.Substring(currentCommandStart, commandDelimiter - currentCommandStart) + .Trim(); + + if (!string.IsNullOrEmpty(commandText)) + { + var command = (JetCommand) ((ICloneable) this).Clone(); + command.CommandText = commandText; + + for (var i = 0; i < usedParameterCount; i++) + { + command.Parameters.RemoveAt(0); + } + + var parameterIndices = parser.GetStateIndices( + new[] {'@', '?'}, + currentCommandStart, + commandDelimiter - currentCommandStart); + + while (command.Parameters.Count > parameterIndices.Count) + { + command.Parameters.RemoveAt(parameterIndices.Count); + } + + usedParameterCount += parameterIndices.Count; + + commands.Add(command); + } + + currentCommandStart = commandDelimiter + 1; + } + } + else + { + commands.Add(this); + } + + return commands.AsReadOnly(); + } + + private DbDataReader TryGetDataReaderForSelectRowCount(string commandText) + { + if (_selectRowCountRegularExpression.Match(commandText) + .Success) + { + if (_rowCount == null) + throw new InvalidOperationException("Invalid " + commandText + ". Run a DataReader before."); + + var dataTable = new DataTable("Rowcount"); + dataTable.Columns.Add("ROWCOUNT", typeof(int)); + dataTable.Rows.Add(_rowCount.Value); + return new DataTableReader(dataTable); + } + + return null; + } + private bool CheckExists(string commandText, out string newCommandText) { var match = _ifStatementRegex.Match(commandText); @@ -345,10 +445,10 @@ namespace System.Data.Jet private void FixParameters() { var parameters = InnerCommand.Parameters; - + if (parameters.Count == 0) return; - + foreach (DbParameter parameter in parameters) { if (parameter.Value is TimeSpan ts) @@ -403,7 +503,8 @@ namespace System.Data.Jet { // We inline all TOP clause parameters of all SELECT statements, because Jet does not support parameters // in TOP clauses. - var parameters = InnerCommand.Parameters.Cast().ToList(); + var parameters = InnerCommand.Parameters.Cast() + .ToList(); if (parameters.Count > 0) { @@ -412,7 +513,10 @@ namespace System.Data.Jet while ((commandText = _topParameterRegularExpression.Replace( lastCommandText, - match => Convert.ToInt32(ExtractParameter(commandText, match.Value, match.Index, parameters).Value).ToString(), + match => Convert.ToInt32( + ExtractParameter(commandText, match.Index, parameters) + .Value) + .ToString(), 1)) != lastCommandText) { lastCommandText = commandText; @@ -429,13 +533,13 @@ namespace System.Data.Jet => fragment.StartsWith("@") || fragment.Equals("?"); - protected virtual DbParameter ExtractParameter(string commandText, string name, int count, List parameters) + protected virtual DbParameter ExtractParameter(string commandText, int count, List parameters) { var indices = GetParameterIndices(commandText.Substring(0, count)); var parameter = InnerCommand.Parameters[indices.Count]; - + parameters.RemoveAt(indices.Count); - + return parameter; } @@ -445,7 +549,7 @@ namespace System.Data.Jet { return; } - + var indices = GetParameterIndices(InnerCommand.CommandText); if (indices.Count <= 0) @@ -454,7 +558,7 @@ namespace System.Data.Jet } var placeholders = GetParameterPlaceholders(InnerCommand.CommandText, indices); - + if (placeholders.All(t => t.Name.StartsWith("@"))) { MatchParametersAndPlaceholders(placeholders); @@ -468,9 +572,11 @@ namespace System.Data.Jet .Insert(placeholder.Index, "?"); } } - + InnerCommand.Parameters.Clear(); - InnerCommand.Parameters.AddRange(placeholders.Select(p => p.Parameter).ToArray()); + InnerCommand.Parameters.AddRange( + placeholders.Select(p => p.Parameter) + .ToArray()); } else if (placeholders.All(t => t.Name == "?")) { @@ -524,7 +630,7 @@ namespace System.Data.Jet protected virtual IReadOnlyList GetParameterPlaceholders(string commandText, IEnumerable indices) { var placeholders = new List(); - + foreach (var index in indices) { var match = Regex.Match(commandText.Substring(index), @"^(?:\?|@\w+)"); @@ -533,8 +639,8 @@ namespace System.Data.Jet { throw new InvalidOperationException("Invalid parameter placeholder found."); } - - placeholders.Add(new ParameterPlaceholder{ Index = index, Name = match.Value }); + + placeholders.Add(new ParameterPlaceholder {Index = index, Name = match.Value}); } return placeholders.AsReadOnly(); @@ -581,7 +687,7 @@ namespace System.Data.Jet /// The created object object ICloneable.Clone() => new JetCommand(this); - + protected class ParameterPlaceholder { public int Index { get; set; }