1770 lines
70 KiB
C#
1770 lines
70 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel;
|
|
using System.Data;
|
|
using System.Data.Common;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using System.Globalization;
|
|
using Npgsql.BackendMessages;
|
|
using Npgsql.Logging;
|
|
using Npgsql.Util;
|
|
using NpgsqlTypes;
|
|
using static Npgsql.Util.Statics;
|
|
using System.Collections;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using Npgsql.Internal;
|
|
|
|
namespace Npgsql;
|
|
|
|
/// <summary>
|
|
/// Represents a SQL statement or function (stored procedure) to execute
|
|
/// against a PostgreSQL database. This class cannot be inherited.
|
|
/// </summary>
|
|
// ReSharper disable once RedundantNameQualifier
|
|
[System.ComponentModel.DesignerCategory("")]
|
|
public sealed class NpgsqlCommand : DbCommand, ICloneable, IComponent
|
|
{
|
|
#region Fields
|
|
|
|
NpgsqlConnection? _connection;
|
|
|
|
readonly NpgsqlConnector? _connector;
|
|
|
|
/// <summary>
|
|
/// If this command is (explicitly) prepared, references the connector on which the preparation happened.
|
|
/// Used to detect when the connector was changed (i.e. connection open/close), meaning that the command
|
|
/// is no longer prepared.
|
|
/// </summary>
|
|
NpgsqlConnector? _connectorPreparedOn;
|
|
|
|
string _commandText;
|
|
CommandBehavior _behavior;
|
|
int? _timeout;
|
|
readonly NpgsqlParameterCollection _parameters;
|
|
|
|
/// <summary>
|
|
/// Whether this <see cref="NpgsqlCommand" /> is wrapped by an <see cref="NpgsqlBatch" />.
|
|
/// </summary>
|
|
internal bool IsWrappedByBatch { get; }
|
|
|
|
internal List<NpgsqlBatchCommand> InternalBatchCommands { get; }
|
|
|
|
Activity? CurrentActivity;
|
|
|
|
/// <summary>
|
|
/// Returns details about each statement that this command has executed.
|
|
/// Is only populated when an Execute* method is called.
|
|
/// </summary>
|
|
[Obsolete("Use the new DbBatch API")]
|
|
public IReadOnlyList<NpgsqlBatchCommand> Statements => InternalBatchCommands.AsReadOnly();
|
|
|
|
UpdateRowSource _updateRowSource = UpdateRowSource.Both;
|
|
|
|
bool IsExplicitlyPrepared => _connectorPreparedOn != null;
|
|
|
|
/// <summary>
|
|
/// Whether this command is cached by <see cref="NpgsqlConnection" /> and returned by <see cref="NpgsqlConnection.CreateCommand" />.
|
|
/// </summary>
|
|
internal bool IsCached { get; set; }
|
|
|
|
#if DEBUG
|
|
internal static bool EnableSqlRewriting;
|
|
#else
|
|
internal static readonly bool EnableSqlRewriting;
|
|
#endif
|
|
|
|
static readonly List<NpgsqlParameter> EmptyParameters = new();
|
|
|
|
static readonly SingleThreadSynchronizationContext SingleThreadSynchronizationContext = new("NpgsqlRemainingAsyncSendWorker");
|
|
|
|
static readonly NpgsqlLogger Log = NpgsqlLogManager.CreateLogger(nameof(NpgsqlCommand));
|
|
|
|
#endregion Fields
|
|
|
|
#region Constants
|
|
|
|
internal const int DefaultTimeout = 30;
|
|
|
|
#endregion
|
|
|
|
#region Constructors
|
|
|
|
static NpgsqlCommand()
|
|
=> EnableSqlRewriting = !AppContext.TryGetSwitch("Npgsql.EnableSqlRewriting", out var enabled) || enabled;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="NpgsqlCommand"/> class.
|
|
/// </summary>
|
|
public NpgsqlCommand() : this(null, null, null) {}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="NpgsqlCommand"/> class with the text of the query.
|
|
/// </summary>
|
|
/// <param name="cmdText">The text of the query.</param>
|
|
// ReSharper disable once IntroduceOptionalParameters.Global
|
|
public NpgsqlCommand(string? cmdText) : this(cmdText, null, null) {}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="NpgsqlCommand"/> class with the text of the query and a
|
|
/// <see cref="NpgsqlConnection"/>.
|
|
/// </summary>
|
|
/// <param name="cmdText">The text of the query.</param>
|
|
/// <param name="connection">A <see cref="NpgsqlConnection"/> that represents the connection to a PostgreSQL server.</param>
|
|
// ReSharper disable once IntroduceOptionalParameters.Global
|
|
public NpgsqlCommand(string? cmdText, NpgsqlConnection? connection) : this(cmdText, connection, null) {}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="NpgsqlCommand"/> class with the text of the query, a
|
|
/// <see cref="NpgsqlConnection"/>, and the <see cref="NpgsqlTransaction"/>.
|
|
/// </summary>
|
|
/// <param name="cmdText">The text of the query.</param>
|
|
/// <param name="connection">A <see cref="NpgsqlConnection"/> that represents the connection to a PostgreSQL server.</param>
|
|
/// <param name="transaction">The <see cref="NpgsqlTransaction"/> in which the <see cref="NpgsqlCommand"/> executes.</param>
|
|
public NpgsqlCommand(string? cmdText, NpgsqlConnection? connection, NpgsqlTransaction? transaction)
|
|
{
|
|
GC.SuppressFinalize(this);
|
|
InternalBatchCommands = new List<NpgsqlBatchCommand>(1);
|
|
_parameters = new NpgsqlParameterCollection();
|
|
_commandText = cmdText ?? string.Empty;
|
|
_connection = connection;
|
|
Transaction = transaction;
|
|
CommandType = CommandType.Text;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Used when this <see cref="NpgsqlCommand"/> instance is wrapped inside an <see cref="NpgsqlBatch"/>.
|
|
/// </summary>
|
|
internal NpgsqlCommand(List<NpgsqlBatchCommand> batchCommands)
|
|
{
|
|
GC.SuppressFinalize(this);
|
|
InternalBatchCommands = batchCommands;
|
|
CommandType = CommandType.Text;
|
|
IsWrappedByBatch = true;
|
|
|
|
// These can/should never be used in this mode
|
|
_commandText = null!;
|
|
_parameters = null!;
|
|
}
|
|
|
|
internal NpgsqlCommand(string? cmdText, NpgsqlConnector connector) : this(cmdText)
|
|
=> _connector = connector;
|
|
|
|
/// <summary>
|
|
/// Used when this <see cref="NpgsqlCommand"/> instance is wrapped inside an <see cref="NpgsqlBatch"/>.
|
|
/// </summary>
|
|
internal NpgsqlCommand(NpgsqlConnector connector, List<NpgsqlBatchCommand> batchCommands)
|
|
: this(batchCommands)
|
|
=> _connector = connector;
|
|
|
|
internal static NpgsqlCommand CreateCachedCommand(NpgsqlConnection connection)
|
|
=> new(null, connection) { IsCached = true };
|
|
|
|
#endregion Constructors
|
|
|
|
#region Public properties
|
|
|
|
/// <summary>
|
|
/// Gets or sets the SQL statement or function (stored procedure) to execute at the data source.
|
|
/// </summary>
|
|
/// <value>The Transact-SQL statement or stored procedure to execute. The default is an empty string.</value>
|
|
[AllowNull, DefaultValue("")]
|
|
[Category("Data")]
|
|
public override string CommandText
|
|
{
|
|
get => _commandText;
|
|
set
|
|
{
|
|
Debug.Assert(!IsWrappedByBatch);
|
|
|
|
_commandText = State == CommandState.Idle
|
|
? value ?? string.Empty
|
|
: throw new InvalidOperationException("An open data reader exists for this command.");
|
|
|
|
ResetExplicitPreparation();
|
|
// TODO: Technically should do this also if the parameter list (or type) changes
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the wait time (in seconds) before terminating the attempt to execute a command and generating an error.
|
|
/// </summary>
|
|
/// <value>The time (in seconds) to wait for the command to execute. The default value is 30 seconds.</value>
|
|
[DefaultValue(DefaultTimeout)]
|
|
public override int CommandTimeout
|
|
{
|
|
get => _timeout ?? (_connection?.CommandTimeout ?? DefaultTimeout);
|
|
set
|
|
{
|
|
if (value < 0) {
|
|
throw new ArgumentOutOfRangeException(nameof(value), value, "CommandTimeout can't be less than zero.");
|
|
}
|
|
|
|
_timeout = value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating how the <see cref="NpgsqlCommand.CommandText"/> property is to be interpreted.
|
|
/// </summary>
|
|
/// <value>
|
|
/// One of the <see cref="System.Data.CommandType"/> values. The default is <see cref="System.Data.CommandType.Text"/>.
|
|
/// </value>
|
|
[DefaultValue(CommandType.Text)]
|
|
[Category("Data")]
|
|
public override CommandType CommandType { get; set; }
|
|
|
|
/// <summary>
|
|
/// DB connection.
|
|
/// </summary>
|
|
protected override DbConnection? DbConnection
|
|
{
|
|
get => _connection;
|
|
set => _connection = (NpgsqlConnection?)value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the <see cref="NpgsqlConnection"/> used by this instance of the <see cref="NpgsqlCommand"/>.
|
|
/// </summary>
|
|
/// <value>The connection to a data source. The default value is <see langword="null"/>.</value>
|
|
[DefaultValue(null)]
|
|
[Category("Behavior")]
|
|
public new NpgsqlConnection? Connection
|
|
{
|
|
get => _connection;
|
|
set
|
|
{
|
|
if (_connection == value)
|
|
return;
|
|
|
|
_connection = State == CommandState.Idle
|
|
? value
|
|
: throw new InvalidOperationException("An open data reader exists for this command.");
|
|
|
|
Transaction = null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Design time visible.
|
|
/// </summary>
|
|
public override bool DesignTimeVisible { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets how command results are applied to the DataRow when used by the
|
|
/// DbDataAdapter.Update(DataSet) method.
|
|
/// </summary>
|
|
/// <value>One of the <see cref="System.Data.UpdateRowSource"/> values.</value>
|
|
[Category("Behavior"), DefaultValue(UpdateRowSource.Both)]
|
|
public override UpdateRowSource UpdatedRowSource
|
|
{
|
|
get => _updateRowSource;
|
|
set
|
|
{
|
|
switch (value)
|
|
{
|
|
// validate value (required based on base type contract)
|
|
case UpdateRowSource.None:
|
|
case UpdateRowSource.OutputParameters:
|
|
case UpdateRowSource.FirstReturnedRecord:
|
|
case UpdateRowSource.Both:
|
|
_updateRowSource = value;
|
|
break;
|
|
default:
|
|
throw new ArgumentOutOfRangeException();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns whether this query will execute as a prepared (compiled) query.
|
|
/// </summary>
|
|
public bool IsPrepared =>
|
|
_connectorPreparedOn == (_connection?.Connector ?? _connector) &&
|
|
InternalBatchCommands.Any() && InternalBatchCommands.All(s => s.PreparedStatement?.IsPrepared == true);
|
|
|
|
#endregion Public properties
|
|
|
|
#region Known/unknown Result Types Management
|
|
|
|
/// <summary>
|
|
/// Marks all of the query's result columns as either known or unknown.
|
|
/// Unknown results column are requested them from PostgreSQL in text format, and Npgsql makes no
|
|
/// attempt to parse them. They will be accessible as strings only.
|
|
/// </summary>
|
|
public bool AllResultTypesAreUnknown
|
|
{
|
|
get => _allResultTypesAreUnknown;
|
|
set
|
|
{
|
|
// TODO: Check that this isn't modified after calling prepare
|
|
_unknownResultTypeList = null;
|
|
_allResultTypesAreUnknown = value;
|
|
}
|
|
}
|
|
|
|
bool _allResultTypesAreUnknown;
|
|
|
|
/// <summary>
|
|
/// Marks the query's result columns as known or unknown, on a column-by-column basis.
|
|
/// Unknown results column are requested them from PostgreSQL in text format, and Npgsql makes no
|
|
/// attempt to parse them. They will be accessible as strings only.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// If the query includes several queries (e.g. SELECT 1; SELECT 2), this will only apply to the first
|
|
/// one. The rest of the queries will be fetched and parsed as usual.
|
|
///
|
|
/// The array size must correspond exactly to the number of result columns the query returns, or an
|
|
/// error will be raised.
|
|
/// </remarks>
|
|
public bool[]? UnknownResultTypeList
|
|
{
|
|
get => _unknownResultTypeList;
|
|
set
|
|
{
|
|
// TODO: Check that this isn't modified after calling prepare
|
|
_allResultTypesAreUnknown = false;
|
|
_unknownResultTypeList = value;
|
|
}
|
|
}
|
|
|
|
bool[]? _unknownResultTypeList;
|
|
|
|
#endregion
|
|
|
|
#region Result Types Management
|
|
|
|
/// <summary>
|
|
/// Marks result types to be used when using GetValue on a data reader, on a column-by-column basis.
|
|
/// Used for Entity Framework 5-6 compability.
|
|
/// Only primitive numerical types and DateTimeOffset are supported.
|
|
/// Set the whole array or just a value to null to use default type.
|
|
/// </summary>
|
|
internal Type[]? ObjectResultTypes { get; set; }
|
|
|
|
#endregion
|
|
|
|
#region State management
|
|
|
|
volatile int _state;
|
|
|
|
/// <summary>
|
|
/// The current state of the command
|
|
/// </summary>
|
|
internal CommandState State
|
|
{
|
|
get => (CommandState)_state;
|
|
set
|
|
{
|
|
var newState = (int)value;
|
|
if (newState == _state)
|
|
return;
|
|
_state = newState;
|
|
}
|
|
}
|
|
|
|
void ResetExplicitPreparation() => _connectorPreparedOn = null;
|
|
|
|
#endregion State management
|
|
|
|
#region Parameters
|
|
|
|
/// <summary>
|
|
/// Creates a new instance of an <see cref="System.Data.Common.DbParameter"/> object.
|
|
/// </summary>
|
|
/// <returns>A <see cref="System.Data.Common.DbParameter"/> object.</returns>
|
|
protected override DbParameter CreateDbParameter() => CreateParameter();
|
|
|
|
/// <summary>
|
|
/// Creates a new instance of a <see cref="NpgsqlParameter"/> object.
|
|
/// </summary>
|
|
/// <returns>An <see cref="NpgsqlParameter"/> object.</returns>
|
|
public new NpgsqlParameter CreateParameter() => new();
|
|
|
|
/// <summary>
|
|
/// DB parameter collection.
|
|
/// </summary>
|
|
protected override DbParameterCollection DbParameterCollection => Parameters;
|
|
|
|
/// <summary>
|
|
/// Gets the <see cref="NpgsqlParameterCollection"/>.
|
|
/// </summary>
|
|
/// <value>The parameters of the SQL statement or function (stored procedure). The default is an empty collection.</value>
|
|
public new NpgsqlParameterCollection Parameters => _parameters;
|
|
|
|
#endregion
|
|
|
|
#region DeriveParameters
|
|
|
|
const string DeriveParametersForFunctionQuery = @"
|
|
SELECT
|
|
CASE
|
|
WHEN pg_proc.proargnames IS NULL THEN array_cat(array_fill(''::name,ARRAY[pg_proc.pronargs]),array_agg(pg_attribute.attname ORDER BY pg_attribute.attnum))
|
|
ELSE pg_proc.proargnames
|
|
END AS proargnames,
|
|
pg_proc.proargtypes,
|
|
CASE
|
|
WHEN pg_proc.proallargtypes IS NULL AND (array_agg(pg_attribute.atttypid))[1] IS NOT NULL THEN array_cat(string_to_array(pg_proc.proargtypes::text,' ')::oid[],array_agg(pg_attribute.atttypid ORDER BY pg_attribute.attnum))
|
|
ELSE pg_proc.proallargtypes
|
|
END AS proallargtypes,
|
|
CASE
|
|
WHEN pg_proc.proargmodes IS NULL AND (array_agg(pg_attribute.atttypid))[1] IS NOT NULL THEN array_cat(array_fill('i'::""char"",ARRAY[pg_proc.pronargs]),array_fill('o'::""char"",ARRAY[array_length(array_agg(pg_attribute.atttypid), 1)]))
|
|
ELSE pg_proc.proargmodes
|
|
END AS proargmodes
|
|
FROM pg_proc
|
|
LEFT JOIN pg_type ON pg_proc.prorettype = pg_type.oid
|
|
LEFT JOIN pg_attribute ON pg_type.typrelid = pg_attribute.attrelid AND pg_attribute.attnum >= 1 AND NOT pg_attribute.attisdropped
|
|
WHERE pg_proc.oid = :proname::regproc
|
|
GROUP BY pg_proc.proargnames, pg_proc.proargtypes, pg_proc.proallargtypes, pg_proc.proargmodes, pg_proc.pronargs;
|
|
";
|
|
|
|
internal void DeriveParameters()
|
|
{
|
|
var conn = CheckAndGetConnection();
|
|
Debug.Assert(conn is not null);
|
|
|
|
if (string.IsNullOrEmpty(CommandText))
|
|
throw new InvalidOperationException("CommandText property has not been initialized");
|
|
|
|
using var _ = conn.StartTemporaryBindingScope(out var connector);
|
|
|
|
if (InternalBatchCommands.Any(s => s.PreparedStatement?.IsExplicit == true))
|
|
throw new NpgsqlException("Deriving parameters isn't supported for commands that are already prepared.");
|
|
|
|
// Here we unprepare statements that possibly are auto-prepared
|
|
Unprepare();
|
|
|
|
Parameters.Clear();
|
|
|
|
switch (CommandType)
|
|
{
|
|
case CommandType.Text:
|
|
DeriveParametersForQuery(connector);
|
|
break;
|
|
case CommandType.StoredProcedure:
|
|
DeriveParametersForFunction();
|
|
break;
|
|
default:
|
|
throw new NotSupportedException("Cannot derive parameters for CommandType " + CommandType);
|
|
}
|
|
}
|
|
|
|
void DeriveParametersForFunction()
|
|
{
|
|
using var c = new NpgsqlCommand(DeriveParametersForFunctionQuery, _connection);
|
|
c.Parameters.Add(new NpgsqlParameter("proname", NpgsqlDbType.Text));
|
|
c.Parameters[0].Value = CommandText;
|
|
|
|
string[]? names = null;
|
|
uint[]? types = null;
|
|
char[]? modes = null;
|
|
|
|
using (var rdr = c.ExecuteReader(CommandBehavior.SingleRow | CommandBehavior.SingleResult))
|
|
{
|
|
if (rdr.Read())
|
|
{
|
|
if (!rdr.IsDBNull(0))
|
|
names = rdr.GetFieldValue<string[]>(0);
|
|
if (!rdr.IsDBNull(2))
|
|
types = rdr.GetFieldValue<uint[]>(2);
|
|
if (!rdr.IsDBNull(3))
|
|
modes = rdr.GetFieldValue<char[]>(3);
|
|
if (types == null)
|
|
{
|
|
if (rdr.IsDBNull(1) || rdr.GetFieldValue<uint[]>(1).Length == 0)
|
|
return; // Parameter-less function
|
|
types = rdr.GetFieldValue<uint[]>(1);
|
|
}
|
|
}
|
|
else
|
|
throw new InvalidOperationException($"{CommandText} does not exist in pg_proc");
|
|
}
|
|
|
|
var typeMapper = c._connection!.Connector!.TypeMapper;
|
|
|
|
for (var i = 0; i < types.Length; i++)
|
|
{
|
|
var param = new NpgsqlParameter();
|
|
|
|
var (npgsqlDbType, postgresType) = typeMapper.GetTypeInfoByOid(types[i]);
|
|
|
|
param.DataTypeName = postgresType.DisplayName;
|
|
param.PostgresType = postgresType;
|
|
if (npgsqlDbType.HasValue)
|
|
param.NpgsqlDbType = npgsqlDbType.Value;
|
|
|
|
if (names != null && i < names.Length)
|
|
param.ParameterName = names[i];
|
|
else
|
|
param.ParameterName = "parameter" + (i + 1);
|
|
|
|
if (modes == null) // All params are IN, or server < 8.1.0 (and only IN is supported)
|
|
param.Direction = ParameterDirection.Input;
|
|
else
|
|
{
|
|
param.Direction = modes[i] switch
|
|
{
|
|
'i' => ParameterDirection.Input,
|
|
'o' => ParameterDirection.Output,
|
|
't' => ParameterDirection.Output,
|
|
'b' => ParameterDirection.InputOutput,
|
|
'v' => throw new NotSupportedException("Cannot derive function parameter of type VARIADIC"),
|
|
_ => throw new ArgumentOutOfRangeException("Unknown code in proargmodes while deriving: " + modes[i])
|
|
};
|
|
}
|
|
|
|
Parameters.Add(param);
|
|
}
|
|
}
|
|
|
|
void DeriveParametersForQuery(NpgsqlConnector connector)
|
|
{
|
|
using (connector.StartUserAction())
|
|
{
|
|
Log.Debug($"Deriving Parameters for query: {CommandText}", connector.Id);
|
|
|
|
if (IsWrappedByBatch)
|
|
foreach (var batchCommand in InternalBatchCommands)
|
|
connector.SqlQueryParser.ParseRawQuery(batchCommand, connector.UseConformingStrings, deriveParameters: true);
|
|
else
|
|
connector.SqlQueryParser.ParseRawQuery(this, connector.UseConformingStrings, deriveParameters: true);
|
|
|
|
var sendTask = SendDeriveParameters(connector, false);
|
|
if (sendTask.IsFaulted)
|
|
sendTask.GetAwaiter().GetResult();
|
|
|
|
foreach (var batchCommand in InternalBatchCommands)
|
|
{
|
|
Expect<ParseCompleteMessage>(
|
|
connector.ReadMessage(async: false).GetAwaiter().GetResult(), connector);
|
|
var paramTypeOIDs = Expect<ParameterDescriptionMessage>(
|
|
connector.ReadMessage(async: false).GetAwaiter().GetResult(), connector).TypeOIDs;
|
|
|
|
if (batchCommand.PositionalParameters.Count != paramTypeOIDs.Count)
|
|
{
|
|
connector.SkipUntil(BackendMessageCode.ReadyForQuery);
|
|
Parameters.Clear();
|
|
throw new NpgsqlException("There was a mismatch in the number of derived parameters between the Npgsql SQL parser and the PostgreSQL parser. Please report this as bug to the Npgsql developers (https://github.com/npgsql/npgsql/issues).");
|
|
}
|
|
|
|
for (var i = 0; i < paramTypeOIDs.Count; i++)
|
|
{
|
|
try
|
|
{
|
|
var param = batchCommand.PositionalParameters[i];
|
|
var paramOid = paramTypeOIDs[i];
|
|
|
|
var (npgsqlDbType, postgresType) = connector.TypeMapper.GetTypeInfoByOid(paramOid);
|
|
|
|
if (param.NpgsqlDbType != NpgsqlDbType.Unknown && param.NpgsqlDbType != npgsqlDbType)
|
|
throw new NpgsqlException("The backend parser inferred different types for parameters with the same name. Please try explicit casting within your SQL statement or batch or use different placeholder names.");
|
|
|
|
param.DataTypeName = postgresType.DisplayName;
|
|
param.PostgresType = postgresType;
|
|
if (npgsqlDbType.HasValue)
|
|
param.NpgsqlDbType = npgsqlDbType.Value;
|
|
}
|
|
catch
|
|
{
|
|
connector.SkipUntil(BackendMessageCode.ReadyForQuery);
|
|
Parameters.Clear();
|
|
throw;
|
|
}
|
|
}
|
|
|
|
var msg = connector.ReadMessage(async: false).GetAwaiter().GetResult();
|
|
switch (msg.Code)
|
|
{
|
|
case BackendMessageCode.RowDescription:
|
|
case BackendMessageCode.NoData:
|
|
break;
|
|
default:
|
|
throw connector.UnexpectedMessageReceived(msg.Code);
|
|
}
|
|
}
|
|
|
|
Expect<ReadyForQueryMessage>(connector.ReadMessage(async: false).GetAwaiter().GetResult(), connector);
|
|
sendTask.GetAwaiter().GetResult();
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Prepare
|
|
|
|
/// <summary>
|
|
/// Creates a server-side prepared statement on the PostgreSQL server.
|
|
/// This will make repeated future executions of this command much faster.
|
|
/// </summary>
|
|
public override void Prepare() => Prepare(false).GetAwaiter().GetResult();
|
|
|
|
/// <summary>
|
|
/// Creates a server-side prepared statement on the PostgreSQL server.
|
|
/// This will make repeated future executions of this command much faster.
|
|
/// </summary>
|
|
/// <param name="cancellationToken">
|
|
/// An optional token to cancel the asynchronous operation. The default value is <see cref="CancellationToken.None"/>.
|
|
/// </param>
|
|
#if NETSTANDARD2_0
|
|
public Task PrepareAsync(CancellationToken cancellationToken = default)
|
|
#else
|
|
public override Task PrepareAsync(CancellationToken cancellationToken = default)
|
|
#endif
|
|
{
|
|
using (NoSynchronizationContextScope.Enter())
|
|
return Prepare(true, cancellationToken);
|
|
}
|
|
|
|
Task Prepare(bool async, CancellationToken cancellationToken = default)
|
|
{
|
|
var connection = CheckAndGetConnection();
|
|
Debug.Assert(connection is not null);
|
|
if (connection.Settings.Multiplexing)
|
|
throw new NotSupportedException("Explicit preparation not supported with multiplexing");
|
|
var connector = connection.Connector!;
|
|
|
|
var needToPrepare = false;
|
|
|
|
if (IsWrappedByBatch)
|
|
{
|
|
foreach (var batchCommand in InternalBatchCommands)
|
|
{
|
|
batchCommand.Parameters.HasOutputParameters = false;
|
|
batchCommand.Parameters.PlaceholderType = PlaceholderType.NoParameters;
|
|
|
|
foreach (var p in batchCommand.Parameters.InternalList)
|
|
{
|
|
batchCommand.Parameters.CalculatePlaceholderType(p);
|
|
p.Bind(connector.TypeMapper);
|
|
}
|
|
|
|
ProcessRawQuery(connector.SqlQueryParser, connector.UseConformingStrings, batchCommand);
|
|
|
|
needToPrepare = batchCommand.ExplicitPrepare(connector) || needToPrepare;
|
|
}
|
|
|
|
if (Log.IsEnabled(NpgsqlLogLevel.Debug))
|
|
Log.Debug(string.Join("; ", InternalBatchCommands.Select(c => c.CommandText)), connector.Id);
|
|
}
|
|
else
|
|
{
|
|
Parameters.HasOutputParameters = false;
|
|
Parameters.PlaceholderType = PlaceholderType.NoParameters;
|
|
|
|
foreach (var p in Parameters.InternalList)
|
|
{
|
|
Parameters.CalculatePlaceholderType(p);
|
|
p.Bind(connector.TypeMapper);
|
|
}
|
|
|
|
ProcessRawQuery(connector.SqlQueryParser, connector.UseConformingStrings, batchCommand: null);
|
|
|
|
foreach (var batchCommand in InternalBatchCommands)
|
|
needToPrepare = batchCommand.ExplicitPrepare(connector) || needToPrepare;
|
|
|
|
if (Log.IsEnabled(NpgsqlLogLevel.Debug))
|
|
Log.Debug($"Preparing: {CommandText}", connector.Id);
|
|
}
|
|
|
|
_connectorPreparedOn = connector;
|
|
|
|
// It's possible the command was already prepared, or that persistent prepared statements were found for
|
|
// all statements. Nothing to do here, move along.
|
|
return needToPrepare
|
|
? PrepareLong(this, async, connector, cancellationToken)
|
|
: Task.CompletedTask;
|
|
|
|
static async Task PrepareLong(NpgsqlCommand command, bool async, NpgsqlConnector connector, CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
using (connector.StartUserAction(cancellationToken))
|
|
{
|
|
var sendTask = command.SendPrepare(connector, async, CancellationToken.None);
|
|
if (sendTask.IsFaulted)
|
|
sendTask.GetAwaiter().GetResult();
|
|
|
|
// Loop over statements, skipping those that are already prepared (because they were persisted)
|
|
var isFirst = true;
|
|
foreach (var batchCommand in command.InternalBatchCommands)
|
|
{
|
|
if (!batchCommand.IsPreparing)
|
|
continue;
|
|
|
|
var pStatement = batchCommand.PreparedStatement!;
|
|
|
|
if (pStatement.StatementBeingReplaced != null)
|
|
{
|
|
Expect<CloseCompletedMessage>(await connector.ReadMessage(async), connector);
|
|
pStatement.StatementBeingReplaced.CompleteUnprepare();
|
|
pStatement.StatementBeingReplaced = null;
|
|
}
|
|
|
|
Expect<ParseCompleteMessage>(await connector.ReadMessage(async), connector);
|
|
Expect<ParameterDescriptionMessage>(await connector.ReadMessage(async), connector);
|
|
var msg = await connector.ReadMessage(async);
|
|
switch (msg.Code)
|
|
{
|
|
case BackendMessageCode.RowDescription:
|
|
// Clone the RowDescription for use with the prepared statement (the one we have is reused
|
|
// by the connection)
|
|
var description = ((RowDescriptionMessage)msg).Clone();
|
|
command.FixupRowDescription(description, isFirst);
|
|
batchCommand.Description = description;
|
|
break;
|
|
case BackendMessageCode.NoData:
|
|
batchCommand.Description = null;
|
|
break;
|
|
default:
|
|
throw connector.UnexpectedMessageReceived(msg.Code);
|
|
}
|
|
|
|
pStatement.State = PreparedState.Prepared;
|
|
connector.PreparedStatementManager.NumPrepared++;
|
|
batchCommand.IsPreparing = false;
|
|
isFirst = false;
|
|
}
|
|
|
|
Expect<ReadyForQueryMessage>(await connector.ReadMessage(async), connector);
|
|
|
|
if (async)
|
|
await sendTask;
|
|
else
|
|
sendTask.GetAwaiter().GetResult();
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// The statements weren't prepared successfully, update the bookkeeping for them
|
|
foreach (var batchCommand in command.InternalBatchCommands)
|
|
{
|
|
if (batchCommand.IsPreparing)
|
|
{
|
|
batchCommand.IsPreparing = false;
|
|
batchCommand.PreparedStatement!.AbortPrepare();
|
|
}
|
|
}
|
|
|
|
throw;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Unprepares a command, closing server-side statements associated with it.
|
|
/// Note that this only affects commands explicitly prepared with <see cref="Prepare()"/>, not
|
|
/// automatically prepared statements.
|
|
/// </summary>
|
|
public void Unprepare()
|
|
=> Unprepare(false).GetAwaiter().GetResult();
|
|
|
|
/// <summary>
|
|
/// Unprepares a command, closing server-side statements associated with it.
|
|
/// Note that this only affects commands explicitly prepared with <see cref="Prepare()"/>, not
|
|
/// automatically prepared statements.
|
|
/// </summary>
|
|
/// <param name="cancellationToken">
|
|
/// An optional token to cancel the asynchronous operation. The default value is <see cref="CancellationToken.None"/>.
|
|
/// </param>
|
|
public Task UnprepareAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
using (NoSynchronizationContextScope.Enter())
|
|
return Unprepare(true, cancellationToken);
|
|
}
|
|
|
|
async Task Unprepare(bool async, CancellationToken cancellationToken = default)
|
|
{
|
|
var connection = CheckAndGetConnection();
|
|
Debug.Assert(connection is not null);
|
|
if (connection.Settings.Multiplexing)
|
|
throw new NotSupportedException("Explicit preparation not supported with multiplexing");
|
|
if (InternalBatchCommands.All(s => !s.IsPrepared))
|
|
return;
|
|
|
|
var connector = connection.Connector!;
|
|
|
|
Log.Debug("Closing command's prepared statements", connector.Id);
|
|
|
|
using (connector.StartUserAction(cancellationToken))
|
|
{
|
|
var sendTask = SendClose(connector, async, cancellationToken);
|
|
if (sendTask.IsFaulted)
|
|
sendTask.GetAwaiter().GetResult();
|
|
|
|
foreach (var batchCommand in InternalBatchCommands)
|
|
{
|
|
if (batchCommand.PreparedStatement?.State == PreparedState.BeingUnprepared)
|
|
{
|
|
Expect<CloseCompletedMessage>(await connector.ReadMessage(async), connector);
|
|
|
|
var pStatement = batchCommand.PreparedStatement;
|
|
pStatement.CompleteUnprepare();
|
|
|
|
if (!pStatement.IsExplicit)
|
|
connector.PreparedStatementManager.AutoPrepared[pStatement.AutoPreparedSlotIndex] = null;
|
|
|
|
batchCommand.PreparedStatement = null;
|
|
}
|
|
}
|
|
|
|
Expect<ReadyForQueryMessage>(await connector.ReadMessage(async), connector);
|
|
|
|
if (async)
|
|
await sendTask;
|
|
else
|
|
sendTask.GetAwaiter().GetResult();
|
|
}
|
|
}
|
|
|
|
#endregion Prepare
|
|
|
|
#region Query analysis
|
|
|
|
internal void ProcessRawQuery(SqlQueryParser? parser, bool standardConformingStrings, NpgsqlBatchCommand? batchCommand)
|
|
{
|
|
var (commandText, commandType, parameters) = batchCommand is null
|
|
? (CommandText, CommandType, Parameters)
|
|
: (batchCommand.CommandText, batchCommand.CommandType, batchCommand.Parameters);
|
|
|
|
if (string.IsNullOrEmpty(commandText))
|
|
throw new InvalidOperationException("CommandText property has not been initialized");
|
|
|
|
switch (commandType)
|
|
{
|
|
case CommandType.Text:
|
|
switch (parameters.PlaceholderType)
|
|
{
|
|
case PlaceholderType.Positional:
|
|
// In positional parameter mode, we don't need to parse/rewrite the CommandText or reorder the parameters - just use
|
|
// them as is. If the SQL contains a semicolon (legacy batching) when positional parameters are in use, we just send
|
|
// that and PostgreSQL will error (this behavior is by-design - use the new batching API).
|
|
if (batchCommand is null)
|
|
{
|
|
batchCommand = TruncateStatementsToOne();
|
|
batchCommand.FinalCommandText = CommandText;
|
|
batchCommand.PositionalParameters = Parameters.InternalList;
|
|
}
|
|
else
|
|
{
|
|
batchCommand.FinalCommandText = batchCommand.CommandText;
|
|
batchCommand.PositionalParameters = batchCommand.Parameters.InternalList;
|
|
}
|
|
|
|
ValidateParameterCount(batchCommand);
|
|
break;
|
|
|
|
case PlaceholderType.NoParameters:
|
|
// Unless the EnableSqlRewriting AppContext switch is explicitly disabled, queries with no parameters are parsed just
|
|
// like queries with named parameters, since they may contain a semicolon (legacy batching).
|
|
if (EnableSqlRewriting)
|
|
goto case PlaceholderType.Named;
|
|
else
|
|
goto case PlaceholderType.Positional;
|
|
|
|
case PlaceholderType.Named:
|
|
if (!EnableSqlRewriting)
|
|
throw new NotSupportedException($"Named parameters are not supported when Npgsql.{nameof(EnableSqlRewriting)} is disabled");
|
|
|
|
// The parser is cached on NpgsqlConnector - unless we're in multiplexing mode.
|
|
parser ??= new SqlQueryParser();
|
|
|
|
if (batchCommand is null)
|
|
{
|
|
parser.ParseRawQuery(this, standardConformingStrings);
|
|
if (InternalBatchCommands.Count > 1 && _parameters.HasOutputParameters)
|
|
throw new NotSupportedException("Commands with multiple queries cannot have out parameters");
|
|
for (var i = 0; i < InternalBatchCommands.Count; i++)
|
|
ValidateParameterCount(InternalBatchCommands[i]);
|
|
}
|
|
else
|
|
{
|
|
parser.ParseRawQuery(batchCommand, standardConformingStrings);
|
|
if (batchCommand.Parameters.HasOutputParameters)
|
|
throw new NotSupportedException("Batches cannot cannot have out parameters");
|
|
ValidateParameterCount(batchCommand);
|
|
}
|
|
|
|
break;
|
|
|
|
case PlaceholderType.Mixed:
|
|
throw new NotSupportedException("Mixing named and positional parameters isn't supported");
|
|
|
|
default:
|
|
throw new ArgumentOutOfRangeException(
|
|
nameof(PlaceholderType), $"Unknown {nameof(PlaceholderType)} value: {Parameters.PlaceholderType}");
|
|
}
|
|
|
|
break;
|
|
|
|
case CommandType.TableDirect:
|
|
batchCommand ??= TruncateStatementsToOne();
|
|
batchCommand.FinalCommandText = "SELECT * FROM " + CommandText;
|
|
break;
|
|
|
|
case CommandType.StoredProcedure:
|
|
var inputList = parameters.Where(p => p.IsInputDirection).ToList();
|
|
var numInput = inputList.Count;
|
|
var sb = new StringBuilder();
|
|
sb.Append("SELECT * FROM ");
|
|
sb.Append(CommandText);
|
|
sb.Append('(');
|
|
var hasWrittenFirst = false;
|
|
for (var i = 1; i <= numInput; i++) {
|
|
var param = inputList[i - 1];
|
|
if (param.IsPositional)
|
|
{
|
|
if (hasWrittenFirst)
|
|
sb.Append(',');
|
|
sb.Append('$');
|
|
sb.Append(i);
|
|
hasWrittenFirst = true;
|
|
}
|
|
}
|
|
for (var i = 1; i <= numInput; i++)
|
|
{
|
|
var param = inputList[i - 1];
|
|
if (!param.IsPositional)
|
|
{
|
|
if (hasWrittenFirst)
|
|
sb.Append(',');
|
|
sb.Append('"');
|
|
sb.Append(param.TrimmedName.Replace("\"", "\"\""));
|
|
sb.Append("\" := ");
|
|
sb.Append('$');
|
|
sb.Append(i);
|
|
hasWrittenFirst = true;
|
|
}
|
|
}
|
|
sb.Append(')');
|
|
|
|
batchCommand ??= TruncateStatementsToOne();
|
|
batchCommand.FinalCommandText = sb.ToString();
|
|
batchCommand.PositionalParameters.AddRange(inputList);
|
|
ValidateParameterCount(batchCommand);
|
|
break;
|
|
|
|
default:
|
|
throw new InvalidOperationException($"Internal Npgsql bug: unexpected value {CommandType} of enum {nameof(CommandType)}. Please file a bug.");
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
static void ValidateParameterCount(NpgsqlBatchCommand batchCommand)
|
|
{
|
|
if (batchCommand.PositionalParameters.Count > ushort.MaxValue)
|
|
throw new NpgsqlException($"A statement cannot have more than {ushort.MaxValue} parameters");
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Message Creation / Population
|
|
|
|
void BeginSend(NpgsqlConnector connector)
|
|
=> connector.WriteBuffer.Timeout = TimeSpan.FromSeconds(CommandTimeout);
|
|
|
|
internal Task Write(NpgsqlConnector connector, bool async, bool flush, CancellationToken cancellationToken = default)
|
|
{
|
|
return (_behavior & CommandBehavior.SchemaOnly) == 0
|
|
? WriteExecute(connector, async, flush, cancellationToken)
|
|
: WriteExecuteSchemaOnly(connector, async, flush, cancellationToken);
|
|
|
|
async Task WriteExecute(NpgsqlConnector connector, bool async, bool flush, CancellationToken cancellationToken)
|
|
{
|
|
for (var i = 0; i < InternalBatchCommands.Count; i++)
|
|
{
|
|
// The following is only for deadlock avoidance when doing sync I/O (so never in multiplexing)
|
|
ForceAsyncIfNecessary(ref async, i);
|
|
|
|
var batchCommand = InternalBatchCommands[i];
|
|
var pStatement = batchCommand.PreparedStatement;
|
|
|
|
Debug.Assert(batchCommand.FinalCommandText is not null);
|
|
|
|
if (pStatement == null || batchCommand.IsPreparing)
|
|
{
|
|
// The statement should either execute unprepared, or is being auto-prepared.
|
|
// Send Parse, Bind, Describe
|
|
|
|
// We may have a prepared statement that replaces an existing statement - close the latter first.
|
|
if (pStatement?.StatementBeingReplaced != null)
|
|
await connector.WriteClose(StatementOrPortal.Statement, pStatement.StatementBeingReplaced.Name!, async, cancellationToken);
|
|
|
|
await connector.WriteParse(batchCommand.FinalCommandText, batchCommand.StatementName, batchCommand.PositionalParameters, async, cancellationToken);
|
|
|
|
await connector.WriteBind(
|
|
batchCommand.PositionalParameters, string.Empty, batchCommand.StatementName, AllResultTypesAreUnknown,
|
|
i == 0 ? UnknownResultTypeList : null,
|
|
async, cancellationToken);
|
|
|
|
await connector.WriteDescribe(StatementOrPortal.Portal, string.Empty, async, cancellationToken);
|
|
}
|
|
else
|
|
{
|
|
// The statement is already prepared, only a Bind is needed
|
|
await connector.WriteBind(
|
|
batchCommand.PositionalParameters, string.Empty, batchCommand.StatementName, AllResultTypesAreUnknown,
|
|
i == 0 ? UnknownResultTypeList : null,
|
|
async, cancellationToken);
|
|
}
|
|
|
|
await connector.WriteExecute(0, async, cancellationToken);
|
|
|
|
if (pStatement != null)
|
|
pStatement.LastUsed = DateTime.UtcNow;
|
|
}
|
|
|
|
await connector.WriteSync(async, cancellationToken);
|
|
|
|
if (flush)
|
|
await connector.Flush(async, cancellationToken);
|
|
}
|
|
|
|
async Task WriteExecuteSchemaOnly(NpgsqlConnector connector, bool async, bool flush, CancellationToken cancellationToken)
|
|
{
|
|
var wroteSomething = false;
|
|
for (var i = 0; i < InternalBatchCommands.Count; i++)
|
|
{
|
|
ForceAsyncIfNecessary(ref async, i);
|
|
|
|
var batchCommand = InternalBatchCommands[i];
|
|
|
|
if (batchCommand.PreparedStatement?.State == PreparedState.Prepared)
|
|
continue; // Prepared, we already have the RowDescription
|
|
|
|
await connector.WriteParse(batchCommand.FinalCommandText!, batchCommand.StatementName, batchCommand.PositionalParameters, async, cancellationToken);
|
|
await connector.WriteDescribe(StatementOrPortal.Statement, batchCommand.StatementName, async, cancellationToken);
|
|
wroteSomething = true;
|
|
}
|
|
|
|
if (wroteSomething)
|
|
{
|
|
await connector.WriteSync(async, cancellationToken);
|
|
if (flush)
|
|
await connector.Flush(async, cancellationToken);
|
|
}
|
|
}
|
|
}
|
|
|
|
async Task SendDeriveParameters(NpgsqlConnector connector, bool async, CancellationToken cancellationToken = default)
|
|
{
|
|
BeginSend(connector);
|
|
|
|
for (var i = 0; i < InternalBatchCommands.Count; i++)
|
|
{
|
|
ForceAsyncIfNecessary(ref async, i);
|
|
|
|
var batchCommand = InternalBatchCommands[i];
|
|
|
|
await connector.WriteParse(batchCommand.FinalCommandText!, string.Empty, EmptyParameters, async, cancellationToken);
|
|
await connector.WriteDescribe(StatementOrPortal.Statement, string.Empty, async, cancellationToken);
|
|
}
|
|
|
|
await connector.WriteSync(async, cancellationToken);
|
|
await connector.Flush(async, cancellationToken);
|
|
}
|
|
|
|
async Task SendPrepare(NpgsqlConnector connector, bool async, CancellationToken cancellationToken = default)
|
|
{
|
|
BeginSend(connector);
|
|
|
|
for (var i = 0; i < InternalBatchCommands.Count; i++)
|
|
{
|
|
ForceAsyncIfNecessary(ref async, i);
|
|
|
|
var batchCommand = InternalBatchCommands[i];
|
|
var pStatement = batchCommand.PreparedStatement;
|
|
|
|
// A statement may be already prepared, already in preparation (i.e. same statement twice
|
|
// in the same command), or we can't prepare (overloaded SQL)
|
|
if (!batchCommand.IsPreparing)
|
|
continue;
|
|
|
|
// We may have a prepared statement that replaces an existing statement - close the latter first.
|
|
var statementToClose = pStatement!.StatementBeingReplaced;
|
|
if (statementToClose != null)
|
|
await connector.WriteClose(StatementOrPortal.Statement, statementToClose.Name!, async, cancellationToken);
|
|
|
|
await connector.WriteParse(batchCommand.FinalCommandText!, pStatement.Name!, batchCommand.PositionalParameters, async, cancellationToken);
|
|
await connector.WriteDescribe(StatementOrPortal.Statement, pStatement.Name!, async, cancellationToken);
|
|
}
|
|
|
|
await connector.WriteSync(async, cancellationToken);
|
|
await connector.Flush(async, cancellationToken);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
void ForceAsyncIfNecessary(ref bool async, int numberOfStatementInBatch)
|
|
{
|
|
if (!async && numberOfStatementInBatch > 0)
|
|
{
|
|
// We're synchronously sending the non-first statement in a batch - switch to async writing.
|
|
// See long comment in Execute() above.
|
|
|
|
// TODO: we can simply do all batch writing asynchronously, instead of starting with the 2nd statement.
|
|
// For now, writing the first statement synchronously gives us a better chance of handle and bubbling up errors correctly
|
|
// (see sendTask.IsFaulted in Execute()). Once #1323 is done, that shouldn't be needed any more and entire batches should
|
|
// be written asynchronously.
|
|
async = true;
|
|
SynchronizationContext.SetSynchronizationContext(SingleThreadSynchronizationContext);
|
|
}
|
|
}
|
|
|
|
async Task SendClose(NpgsqlConnector connector, bool async, CancellationToken cancellationToken = default)
|
|
{
|
|
BeginSend(connector);
|
|
|
|
var i = 0;
|
|
foreach (var batchCommand in InternalBatchCommands.Where(s => s.IsPrepared))
|
|
{
|
|
ForceAsyncIfNecessary(ref async, i);
|
|
|
|
await connector.WriteClose(StatementOrPortal.Statement, batchCommand.StatementName, async, cancellationToken);
|
|
batchCommand.PreparedStatement!.State = PreparedState.BeingUnprepared;
|
|
i++;
|
|
}
|
|
|
|
await connector.WriteSync(async, cancellationToken);
|
|
await connector.Flush(async, cancellationToken);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Execute Non Query
|
|
|
|
/// <summary>
|
|
/// Executes a SQL statement against the connection and returns the number of rows affected.
|
|
/// </summary>
|
|
/// <returns>The number of rows affected if known; -1 otherwise.</returns>
|
|
public override int ExecuteNonQuery() => ExecuteNonQuery(false, CancellationToken.None).GetAwaiter().GetResult();
|
|
|
|
/// <summary>
|
|
/// Asynchronous version of <see cref="ExecuteNonQuery()"/>
|
|
/// </summary>
|
|
/// <param name="cancellationToken">
|
|
/// An optional token to cancel the asynchronous operation. The default value is <see cref="CancellationToken.None"/>.
|
|
/// </param>
|
|
/// <returns>A task representing the asynchronous operation, with the number of rows affected if known; -1 otherwise.</returns>
|
|
public override Task<int> ExecuteNonQueryAsync(CancellationToken cancellationToken)
|
|
{
|
|
using (NoSynchronizationContextScope.Enter())
|
|
return ExecuteNonQuery(true, cancellationToken);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
async Task<int> ExecuteNonQuery(bool async, CancellationToken cancellationToken)
|
|
{
|
|
var reader = await ExecuteReader(CommandBehavior.Default, async, cancellationToken);
|
|
try
|
|
{
|
|
while (async ? await reader.NextResultAsync(cancellationToken) : reader.NextResult()) ;
|
|
|
|
return reader.RecordsAffected;
|
|
}
|
|
finally
|
|
{
|
|
if (async)
|
|
await reader.DisposeAsync();
|
|
else
|
|
reader.Dispose();
|
|
}
|
|
}
|
|
|
|
#endregion Execute Non Query
|
|
|
|
#region Execute Scalar
|
|
|
|
/// <summary>
|
|
/// Executes the query, and returns the first column of the first row
|
|
/// in the result set returned by the query. Extra columns or rows are ignored.
|
|
/// </summary>
|
|
/// <returns>The first column of the first row in the result set,
|
|
/// or a null reference if the result set is empty.</returns>
|
|
public override object? ExecuteScalar() => ExecuteScalar(false, CancellationToken.None).GetAwaiter().GetResult();
|
|
|
|
/// <summary>
|
|
/// Asynchronous version of <see cref="ExecuteScalar()"/>
|
|
/// </summary>
|
|
/// <param name="cancellationToken">
|
|
/// An optional token to cancel the asynchronous operation. The default value is <see cref="CancellationToken.None"/>.
|
|
/// </param>
|
|
/// <returns>A task representing the asynchronous operation, with the first column of the
|
|
/// first row in the result set, or a null reference if the result set is empty.</returns>
|
|
public override Task<object?> ExecuteScalarAsync(CancellationToken cancellationToken)
|
|
{
|
|
using (NoSynchronizationContextScope.Enter())
|
|
return ExecuteScalar(true, cancellationToken).AsTask();
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
async ValueTask<object?> ExecuteScalar(bool async, CancellationToken cancellationToken)
|
|
{
|
|
var behavior = CommandBehavior.SingleRow;
|
|
if (IsWrappedByBatch || !Parameters.HasOutputParameters)
|
|
behavior |= CommandBehavior.SequentialAccess;
|
|
|
|
var reader = await ExecuteReader(behavior, async, cancellationToken);
|
|
try
|
|
{
|
|
var read = async ? await reader.ReadAsync(cancellationToken) : reader.Read();
|
|
var value = read && reader.FieldCount != 0 ? reader.GetValue(0) : null;
|
|
// We read the whole result set to trigger any errors
|
|
while (async ? await reader.NextResultAsync(cancellationToken) : reader.NextResult()) ;
|
|
return value;
|
|
}
|
|
finally
|
|
{
|
|
if (async)
|
|
await reader.DisposeAsync();
|
|
else
|
|
reader.Dispose();
|
|
}
|
|
}
|
|
|
|
#endregion Execute Scalar
|
|
|
|
#region Execute Reader
|
|
|
|
/// <summary>
|
|
/// Executes the command text against the connection.
|
|
/// </summary>
|
|
/// <returns>A task representing the operation.</returns>
|
|
protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior)
|
|
=> ExecuteReader(behavior);
|
|
|
|
/// <summary>
|
|
/// Executes the command text against the connection.
|
|
/// </summary>
|
|
/// <param name="behavior">An instance of <see cref="CommandBehavior"/>.</param>
|
|
/// <param name="cancellationToken">
|
|
/// An optional token to cancel the asynchronous operation. The default value is <see cref="CancellationToken.None"/>.
|
|
/// </param>
|
|
/// <returns>A task representing the asynchronous operation.</returns>
|
|
protected override async Task<DbDataReader> ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken)
|
|
=> await ExecuteReaderAsync(behavior, cancellationToken);
|
|
|
|
/// <summary>
|
|
/// Executes the <see cref="CommandText"/> against the <see cref="Connection"/>
|
|
/// and returns a <see cref="NpgsqlDataReader"/>.
|
|
/// </summary>
|
|
/// <param name="behavior">One of the enumeration values that specified the command behavior.</param>
|
|
/// <returns>A task representing the operation.</returns>
|
|
public new NpgsqlDataReader ExecuteReader(CommandBehavior behavior = CommandBehavior.Default)
|
|
=> ExecuteReader(behavior, async: false, CancellationToken.None).GetAwaiter().GetResult();
|
|
|
|
/// <summary>
|
|
/// An asynchronous version of <see cref="ExecuteReader(CommandBehavior)"/>, which executes
|
|
/// the <see cref="CommandText"/> against the <see cref="Connection"/>
|
|
/// and returns a <see cref="NpgsqlDataReader"/>.
|
|
/// </summary>
|
|
/// <param name="cancellationToken">
|
|
/// An optional token to cancel the asynchronous operation. The default value is <see cref="CancellationToken.None"/>.
|
|
/// </param>
|
|
/// <returns>A task representing the asynchronous operation.</returns>
|
|
public new Task<NpgsqlDataReader> ExecuteReaderAsync(CancellationToken cancellationToken = default)
|
|
=> ExecuteReaderAsync(CommandBehavior.Default, cancellationToken);
|
|
|
|
/// <summary>
|
|
/// An asynchronous version of <see cref="ExecuteReader(CommandBehavior)"/>,
|
|
/// which executes the <see cref="CommandText"/> against the <see cref="Connection"/>
|
|
/// and returns a <see cref="NpgsqlDataReader"/>.
|
|
/// </summary>
|
|
/// <param name="behavior">One of the enumeration values that specified the command behavior.</param>
|
|
/// <param name="cancellationToken">
|
|
/// An optional token to cancel the asynchronous operation. The default value is <see cref="CancellationToken.None"/>.
|
|
/// </param>
|
|
/// <returns>A task representing the asynchronous operation.</returns>
|
|
public new Task<NpgsqlDataReader> ExecuteReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken = default)
|
|
{
|
|
using (NoSynchronizationContextScope.Enter())
|
|
return ExecuteReader(behavior, async: true, cancellationToken).AsTask();
|
|
}
|
|
|
|
// TODO: Maybe pool these?
|
|
internal ManualResetValueTaskSource<NpgsqlConnector> ExecutionCompletion { get; }
|
|
= new();
|
|
|
|
internal async ValueTask<NpgsqlDataReader> ExecuteReader(CommandBehavior behavior, bool async, CancellationToken cancellationToken)
|
|
{
|
|
var conn = CheckAndGetConnection();
|
|
_behavior = behavior;
|
|
|
|
NpgsqlConnector? connector;
|
|
if (_connector is not null)
|
|
{
|
|
Debug.Assert(conn is null);
|
|
if (behavior.HasFlag(CommandBehavior.CloseConnection))
|
|
throw new ArgumentException($"{nameof(CommandBehavior.CloseConnection)} is not supported with ${nameof(NpgsqlConnector)}", nameof(behavior));
|
|
connector = _connector;
|
|
}
|
|
else
|
|
{
|
|
Debug.Assert(conn is not null);
|
|
conn.TryGetBoundConnector(out connector);
|
|
}
|
|
|
|
try
|
|
{
|
|
if (connector is not null)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
// We cannot pass a token here, as we'll cancel a non-send query
|
|
// Also, we don't pass the cancellation token to StartUserAction, since that would make it scope to the entire action (command execution)
|
|
// whereas it should only be scoped to the Execute method.
|
|
connector.StartUserAction(ConnectorState.Executing, this, CancellationToken.None);
|
|
|
|
Task? sendTask;
|
|
|
|
try
|
|
{
|
|
switch (IsExplicitlyPrepared)
|
|
{
|
|
case true:
|
|
Debug.Assert(_connectorPreparedOn != null);
|
|
if (IsWrappedByBatch)
|
|
{
|
|
foreach (var batchCommand in InternalBatchCommands)
|
|
{
|
|
if (batchCommand.ConnectorPreparedOn != connector)
|
|
{
|
|
foreach (var s in InternalBatchCommands)
|
|
s.ResetPreparation();
|
|
ResetExplicitPreparation();
|
|
goto case false;
|
|
}
|
|
|
|
batchCommand.Parameters.ValidateAndBind(connector.TypeMapper);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (_connectorPreparedOn != connector)
|
|
{
|
|
// The command was prepared, but since then the connector has changed. Detach all prepared statements.
|
|
foreach (var s in InternalBatchCommands)
|
|
s.PreparedStatement = null;
|
|
ResetExplicitPreparation();
|
|
goto case false;
|
|
}
|
|
Parameters.ValidateAndBind(connector.TypeMapper);
|
|
}
|
|
|
|
NpgsqlEventSource.Log.CommandStartPrepared();
|
|
break;
|
|
|
|
case false:
|
|
var numPrepared = 0;
|
|
|
|
if (IsWrappedByBatch)
|
|
{
|
|
for (var i = 0; i < InternalBatchCommands.Count; i++)
|
|
{
|
|
var batchCommand = InternalBatchCommands[i];
|
|
|
|
batchCommand.Parameters.ValidateAndBind(connector.TypeMapper);
|
|
ProcessRawQuery(connector.SqlQueryParser, connector.UseConformingStrings, batchCommand);
|
|
|
|
if (connector.Settings.MaxAutoPrepare > 0 && batchCommand.TryAutoPrepare(connector))
|
|
numPrepared++;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Parameters.ValidateAndBind(connector.TypeMapper);
|
|
ProcessRawQuery(connector.SqlQueryParser, connector.UseConformingStrings, batchCommand: null);
|
|
|
|
if (connector.Settings.MaxAutoPrepare > 0)
|
|
for (var i = 0; i < InternalBatchCommands.Count; i++)
|
|
if (InternalBatchCommands[i].TryAutoPrepare(connector))
|
|
numPrepared++;
|
|
}
|
|
|
|
if (numPrepared > 0)
|
|
{
|
|
_connectorPreparedOn = connector;
|
|
if (numPrepared == InternalBatchCommands.Count)
|
|
NpgsqlEventSource.Log.CommandStartPrepared();
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
State = CommandState.InProgress;
|
|
|
|
if (Log.IsEnabled(NpgsqlLogLevel.Debug))
|
|
LogCommand(connector);
|
|
NpgsqlEventSource.Log.CommandStart(CommandText);
|
|
TraceCommandStart(connector);
|
|
|
|
// If a cancellation is in progress, wait for it to "complete" before proceeding (#615)
|
|
connector.ResetCancellation();
|
|
|
|
// We do not wait for the entire send to complete before proceeding to reading -
|
|
// the sending continues in parallel with the user's reading. Waiting for the
|
|
// entire send to complete would trigger a deadlock for multi-statement commands,
|
|
// where PostgreSQL sends large results for the first statement, while we're sending large
|
|
// parameter data for the second. See #641.
|
|
// Instead, all sends for non-first statements are performed asynchronously (even if the user requested sync),
|
|
// in a special synchronization context to prevents a dependency on the thread pool (which would also trigger
|
|
// deadlocks).
|
|
BeginSend(connector);
|
|
sendTask = Write(connector, async, flush: true, CancellationToken.None);
|
|
|
|
// The following is a hack. It raises an exception if one was thrown in the first phases
|
|
// of the send (i.e. in parts of the send that executed synchronously). Exceptions may
|
|
// still happen later and aren't properly handled. See #1323.
|
|
if (sendTask.IsFaulted)
|
|
sendTask.GetAwaiter().GetResult();
|
|
}
|
|
catch
|
|
{
|
|
connector.EndUserAction();
|
|
throw;
|
|
}
|
|
|
|
// TODO: DRY the following with multiplexing, but be careful with the cancellation registration...
|
|
var reader = connector.DataReader;
|
|
reader.Init(this, behavior, InternalBatchCommands, sendTask);
|
|
connector.CurrentReader = reader;
|
|
if (async)
|
|
await reader.NextResultAsync(cancellationToken);
|
|
else
|
|
reader.NextResult();
|
|
|
|
TraceReceivedFirstResponse();
|
|
|
|
return reader;
|
|
}
|
|
else
|
|
{
|
|
Debug.Assert(conn is not null);
|
|
Debug.Assert(conn.Settings.Multiplexing);
|
|
// The connection isn't bound to a connector - it's multiplexing time.
|
|
var pool = (MultiplexingConnectorPool)conn.Pool;
|
|
|
|
if (!async)
|
|
{
|
|
// The waiting on the ExecutionCompletion ManualResetValueTaskSource is necessarily
|
|
// asynchronous, so allowing sync would mean sync-over-async.
|
|
throw new NotSupportedException(
|
|
"Synchronous command execution is not supported when multiplexing is on");
|
|
}
|
|
|
|
if (IsWrappedByBatch)
|
|
{
|
|
foreach (var batchCommand in InternalBatchCommands)
|
|
{
|
|
batchCommand.Parameters.ValidateAndBind(pool.MultiplexingTypeMapper!);
|
|
ProcessRawQuery(null, standardConformingStrings: true, batchCommand);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Parameters.ValidateAndBind(pool.MultiplexingTypeMapper!);
|
|
ProcessRawQuery(null, standardConformingStrings: true, batchCommand: null);
|
|
}
|
|
|
|
State = CommandState.InProgress;
|
|
|
|
// TODO: Experiment: do we want to wait on *writing* here, or on *reading*?
|
|
// Previous behavior was to wait on reading, which throw the exception from ExecuteReader (and not from
|
|
// the first read). But waiting on writing would allow us to do sync writing and async reading.
|
|
ExecutionCompletion.Reset();
|
|
await pool.MultiplexCommandWriter!.WriteAsync(this, cancellationToken);
|
|
connector = await new ValueTask<NpgsqlConnector>(ExecutionCompletion, ExecutionCompletion.Version);
|
|
// TODO: Overload of StartBindingScope?
|
|
conn.Connector = connector;
|
|
connector.Connection = conn;
|
|
conn.ConnectorBindingScope = ConnectorBindingScope.Reader;
|
|
|
|
var reader = connector.DataReader;
|
|
reader.Init(this, behavior, InternalBatchCommands);
|
|
connector.CurrentReader = reader;
|
|
await reader.NextResultAsync(cancellationToken);
|
|
|
|
return reader;
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
var reader = connector?.CurrentReader;
|
|
if (!(e is NpgsqlOperationInProgressException) && reader != null)
|
|
await reader.Cleanup(async);
|
|
|
|
TraceSetException(e);
|
|
|
|
State = CommandState.Idle;
|
|
|
|
// Reader disposal contains logic for closing the connection if CommandBehavior.CloseConnection is
|
|
// specified. However, close here as well in case of an error before the reader was even instantiated
|
|
// (e.g. write I/O error)
|
|
if ((behavior & CommandBehavior.CloseConnection) == CommandBehavior.CloseConnection)
|
|
{
|
|
Debug.Assert(_connector is null && conn is not null);
|
|
conn.Close();
|
|
}
|
|
|
|
throw;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Transactions
|
|
|
|
/// <summary>
|
|
/// DB transaction.
|
|
/// </summary>
|
|
protected override DbTransaction? DbTransaction
|
|
{
|
|
get => Transaction;
|
|
set => Transaction = (NpgsqlTransaction?)value;
|
|
}
|
|
/// <summary>
|
|
/// This property is ignored by Npgsql. PostgreSQL only supports a single transaction at a given time on
|
|
/// a given connection, and all commands implicitly run inside the current transaction started via
|
|
/// <see cref="NpgsqlConnection.BeginTransaction()"/>
|
|
/// </summary>
|
|
public new NpgsqlTransaction? Transaction { get; set; }
|
|
|
|
#endregion Transactions
|
|
|
|
#region Cancel
|
|
|
|
/// <summary>
|
|
/// Attempts to cancel the execution of an <see cref="NpgsqlCommand" />.
|
|
/// </summary>
|
|
/// <remarks>As per the specs, no exception will be thrown by this method in case of failure.</remarks>
|
|
public override void Cancel()
|
|
{
|
|
if (State != CommandState.InProgress)
|
|
return;
|
|
|
|
var connector = Connection?.Connector ?? _connector;
|
|
if (connector is null)
|
|
return;
|
|
|
|
connector.PerformUserCancellation();
|
|
}
|
|
|
|
#endregion Cancel
|
|
|
|
#region Dispose
|
|
|
|
/// <summary>
|
|
/// Releases the resources used by the <see cref="NpgsqlCommand"/>.
|
|
/// </summary>
|
|
protected override void Dispose(bool disposing)
|
|
{
|
|
Transaction = null;
|
|
|
|
State = CommandState.Disposed;
|
|
|
|
if (IsCached && _connection is not null && _connection.CachedCommand is null)
|
|
{
|
|
// TODO: Optimize NpgsqlParameterCollection to recycle NpgsqlParameter instances as well
|
|
// TODO: Statements isn't cleared/recycled, leaving this for now, since it'll be replaced by the new batching API
|
|
|
|
_commandText = string.Empty;
|
|
CommandType = CommandType.Text;
|
|
_parameters.Clear();
|
|
_connection.CachedCommand = this;
|
|
return;
|
|
}
|
|
|
|
IsCached = false;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Tracing
|
|
|
|
#endregion Tracing
|
|
|
|
internal void TraceCommandStart(NpgsqlConnector connector)
|
|
{
|
|
Debug.Assert(CurrentActivity is null);
|
|
if (NpgsqlActivitySource.IsEnabled)
|
|
CurrentActivity = NpgsqlActivitySource.CommandStart(connector, CommandText);
|
|
}
|
|
|
|
internal void TraceReceivedFirstResponse()
|
|
{
|
|
if (CurrentActivity is not null)
|
|
{
|
|
NpgsqlActivitySource.ReceivedFirstResponse(CurrentActivity);
|
|
}
|
|
}
|
|
|
|
internal void TraceCommandStop()
|
|
{
|
|
if (CurrentActivity is not null)
|
|
{
|
|
NpgsqlActivitySource.CommandStop(CurrentActivity);
|
|
CurrentActivity = null;
|
|
}
|
|
}
|
|
|
|
internal void TraceSetException(Exception e)
|
|
{
|
|
if (CurrentActivity is not null)
|
|
{
|
|
NpgsqlActivitySource.SetException(CurrentActivity, e);
|
|
CurrentActivity = null;
|
|
}
|
|
}
|
|
|
|
#region Misc
|
|
|
|
NpgsqlBatchCommand TruncateStatementsToOne()
|
|
{
|
|
switch (InternalBatchCommands.Count)
|
|
{
|
|
case 0:
|
|
var statement = new NpgsqlBatchCommand();
|
|
InternalBatchCommands.Add(statement);
|
|
return statement;
|
|
|
|
case 1:
|
|
statement = InternalBatchCommands[0];
|
|
statement.Reset();
|
|
return statement;
|
|
|
|
default:
|
|
statement = InternalBatchCommands[0];
|
|
statement.Reset();
|
|
InternalBatchCommands.Clear();
|
|
InternalBatchCommands.Add(statement);
|
|
return statement;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fixes up the text/binary flag on result columns.
|
|
/// Since Prepare() describes a statement rather than a portal, the resulting RowDescription
|
|
/// will have text format on all result columns. Fix that up.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Note that UnknownResultTypeList only applies to the first query, while AllResultTypesAreUnknown applies
|
|
/// to all of them.
|
|
/// </remarks>
|
|
internal void FixupRowDescription(RowDescriptionMessage rowDescription, bool isFirst)
|
|
{
|
|
for (var i = 0; i < rowDescription.Count; i++)
|
|
{
|
|
var field = rowDescription[i];
|
|
field.FormatCode = (UnknownResultTypeList == null || !isFirst ? AllResultTypesAreUnknown : UnknownResultTypeList[i])
|
|
? FormatCode.Text
|
|
: FormatCode.Binary;
|
|
field.ResolveHandler();
|
|
}
|
|
}
|
|
|
|
void LogCommand(NpgsqlConnector connector)
|
|
{
|
|
var sb = new StringBuilder();
|
|
sb.AppendLine("Executing statement(s):");
|
|
foreach (var s in InternalBatchCommands)
|
|
{
|
|
sb.Append("\t").AppendLine(s.FinalCommandText);
|
|
var p = s.PositionalParameters;
|
|
if (p.Count > 0 && (NpgsqlLogManager.IsParameterLoggingEnabled || connector.Settings.LogParameters))
|
|
{
|
|
for (var i = 0; i < p.Count; i++)
|
|
{
|
|
sb.Append("\t").Append("Parameter $").Append(i + 1).Append(":");
|
|
switch (p[i].Value)
|
|
{
|
|
case IList list:
|
|
for (var j = 0; j < list.Count; j++)
|
|
{
|
|
sb.Append("\t#").Append(j).Append(": ").Append(Convert.ToString(list[j], CultureInfo.InvariantCulture));
|
|
}
|
|
break;
|
|
case DBNull _:
|
|
case null:
|
|
sb.Append("\t").Append(Convert.ToString("null", CultureInfo.InvariantCulture));
|
|
break;
|
|
default:
|
|
sb.Append("\t").Append(Convert.ToString(p[i].Value, CultureInfo.InvariantCulture));
|
|
break;
|
|
}
|
|
sb.AppendLine();
|
|
}
|
|
}
|
|
}
|
|
Log.Debug(sb.ToString(), connector.Id);
|
|
connector.QueryLogStopWatch.Start();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a new command based on this one.
|
|
/// </summary>
|
|
/// <returns>A new NpgsqlCommand object.</returns>
|
|
object ICloneable.Clone() => Clone();
|
|
|
|
/// <summary>
|
|
/// Create a new command based on this one.
|
|
/// </summary>
|
|
/// <returns>A new NpgsqlCommand object.</returns>
|
|
public NpgsqlCommand Clone()
|
|
{
|
|
var clone = new NpgsqlCommand(CommandText, _connection, Transaction)
|
|
{
|
|
CommandTimeout = CommandTimeout, CommandType = CommandType, DesignTimeVisible = DesignTimeVisible, _allResultTypesAreUnknown = _allResultTypesAreUnknown, _unknownResultTypeList = _unknownResultTypeList, ObjectResultTypes = ObjectResultTypes
|
|
};
|
|
_parameters.CloneTo(clone._parameters);
|
|
return clone;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
NpgsqlConnection? CheckAndGetConnection()
|
|
{
|
|
if (State == CommandState.Disposed)
|
|
throw new ObjectDisposedException(GetType().FullName);
|
|
if (_connection == null)
|
|
{
|
|
if (_connector is null)
|
|
throw new InvalidOperationException("Connection property has not been initialized.");
|
|
return null;
|
|
}
|
|
switch (_connection.FullState)
|
|
{
|
|
case ConnectionState.Open:
|
|
case ConnectionState.Connecting:
|
|
case ConnectionState.Open | ConnectionState.Executing:
|
|
case ConnectionState.Open | ConnectionState.Fetching:
|
|
return _connection;
|
|
default:
|
|
throw new InvalidOperationException("Connection is not open");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// This event is unsupported by Npgsql. Use <see cref="System.Data.Common.DbConnection.StateChange"/> instead.
|
|
/// </summary>
|
|
[EditorBrowsable(EditorBrowsableState.Never)]
|
|
public new event EventHandler? Disposed
|
|
{
|
|
add => throw new NotSupportedException("The Disposed event isn't supported by Npgsql. Use DbConnection.StateChange instead.");
|
|
remove => throw new NotSupportedException("The Disposed event isn't supported by Npgsql. Use DbConnection.StateChange instead.");
|
|
}
|
|
|
|
event EventHandler? IComponent.Disposed
|
|
{
|
|
add => Disposed += value;
|
|
remove => Disposed -= value;
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
|
|
enum CommandState
|
|
{
|
|
Idle,
|
|
InProgress,
|
|
Disposed
|
|
} |