X Tutup
using System; using System.Buffers; using System.Collections.Generic; using System.Data; using System.Data.Common; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using Microsoft.Extensions.Logging; using Npgsql.BackendMessages; using Npgsql.Internal; namespace Npgsql; /// public sealed class NpgsqlBatchCommand : DbBatchCommand { internal static readonly List EmptyParameters = []; string _commandText; /// [AllowNull] public override string CommandText { get => _commandText; set { _commandText = value ?? string.Empty; ResetPreparation(); // TODO: Technically should do this also if the parameter list (or type) changes } } /// public override CommandType CommandType { get; set; } = CommandType.Text; /// protected override DbParameterCollection DbParameterCollection => Parameters; internal NpgsqlParameterCollection? _parameters; /// public new NpgsqlParameterCollection Parameters => _parameters ??= []; internal bool HasOutputParameters => _parameters?.HasOutputParameters == true; /// public override NpgsqlParameter CreateParameter() => new(); /// public override bool CanCreateParameter => true; /// /// Appends an error barrier after this batch command. Defaults to the value of on the /// batch. /// /// /// /// By default, any exception in a command causes later commands in the batch to be skipped, and earlier commands to be rolled back. /// Appending an error barrier ensures that errors from this command (or previous ones) won't cause later commands to be skipped, /// and that errors from later commands won't cause this command (or previous ones) to be rolled back). /// /// /// Note that if the batch is executed within an explicit transaction, the first error places the transaction in a failed state, /// causing all later commands to fail in any case. As a result, this option is useful mainly when there is no explicit transaction. /// /// /// At the PostgreSQL wire protocol level, this corresponds to inserting a Sync message after this command, rather than grouping /// all the batch's commands behind a single terminating Sync. /// /// /// Controlling error barriers on a command-by-command basis is an advanced feature, consider enabling error barriers for the entire /// batch via . /// /// public bool? AppendErrorBarrier { get; set; } /// /// The number of rows affected or retrieved. /// /// /// See the command tag in the CommandComplete message for the meaning of this value for each , /// https://www.postgresql.org/docs/current/static/protocol-message-formats.html /// public ulong Rows { get; internal set; } /// public override int RecordsAffected { get { switch (StatementType) { case StatementType.Update: case StatementType.Insert: case StatementType.Delete: case StatementType.Copy: case StatementType.Move: case StatementType.Merge: return Rows > int.MaxValue ? throw new OverflowException($"The number of records affected exceeds int.MaxValue. Use {nameof(Rows)}.") : (int)Rows; default: return -1; } } } /// /// Specifies the type of query, e.g. SELECT. /// public StatementType StatementType { get; internal set; } /// /// For an INSERT, the object ID of the inserted row if is 1 and /// the target table has OIDs; otherwise 0. /// public uint OID { get; internal set; } /// /// The SQL as it will be sent to PostgreSQL, after any rewriting performed by Npgsql (e.g. named to positional parameter /// placeholders). /// internal string? FinalCommandText { get; set; } /// /// The list of parameters, ordered positionally, as it will be sent to PostgreSQL. /// /// /// If the user provided positional parameters, this references the (in batching mode) or the list /// backing (in non-batching) mode. If the user provided named parameters, this is a /// separate list containing the re-ordered parameters. /// internal List PositionalParameters { get => _inputParameters ??= _ownedInputParameters ??= []; set => _inputParameters = value; } internal bool HasParameters => _inputParameters?.Count > 0 || _ownedInputParameters?.Count > 0; internal List CurrentParametersReadOnly => HasParameters ? PositionalParameters : EmptyParameters; List? _ownedInputParameters; List? _inputParameters; /// /// The RowDescription message for this query. If null, the query does not return rows (e.g. INSERT) /// internal RowDescriptionMessage? Description { get => PreparedStatement == null ? _description : PreparedStatement.Description; set { if (PreparedStatement == null) _description = value; else PreparedStatement.Description = value; } } RowDescriptionMessage? _description; /// /// If this statement has been automatically prepared, references the . /// Null otherwise. /// internal PreparedStatement? PreparedStatement { get => _preparedStatement is { State: PreparedState.Unprepared } ? _preparedStatement = null : _preparedStatement; set => _preparedStatement = value; } PreparedStatement? _preparedStatement; internal NpgsqlConnector? ConnectorPreparedOn { get; set; } internal bool IsPreparing; /// /// Holds the server-side (prepared) ASCII statement name. Empty string for non-prepared statements. /// internal byte[] StatementName => PreparedStatement?.Name ?? []; /// /// Whether this statement has already been prepared (including automatic preparation). /// internal bool IsPrepared => PreparedStatement?.IsPrepared == true; /// /// Returns a prepared statement for this statement (including automatic preparation). /// internal bool TryGetPrepared([NotNullWhen(true)] out PreparedStatement? preparedStatement) { preparedStatement = PreparedStatement; return preparedStatement?.IsPrepared == true; } /// /// Initializes a new . /// public NpgsqlBatchCommand() : this(string.Empty) {} /// /// Initializes a new . /// /// The text of the . public NpgsqlBatchCommand(string commandText) => _commandText = commandText; internal bool ExplicitPrepare(NpgsqlConnector connector) { if (!IsPrepared) { PreparedStatement = connector.PreparedStatementManager.GetOrAddExplicit(this); if (PreparedStatement?.State == PreparedState.NotPrepared) { PreparedStatement.State = PreparedState.BeingPrepared; IsPreparing = true; return true; } } return false; } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal bool TryAutoPrepare(NpgsqlConnector connector) { // If this statement isn't prepared, see if it gets implicitly prepared. // Note that this may return null (not enough usages for automatic preparation). if (!TryGetPrepared(out var preparedStatement)) preparedStatement = PreparedStatement = connector.PreparedStatementManager.TryGetAutoPrepared(this); if (preparedStatement is not null) { if (preparedStatement.State == PreparedState.NotPrepared) { preparedStatement.State = PreparedState.BeingPrepared; IsPreparing = true; } return true; } return false; } internal void Reset() { CommandText = string.Empty; StatementType = StatementType.Select; _description = null; Rows = 0; OID = 0; PreparedStatement = null; if (ReferenceEquals(_inputParameters, _ownedInputParameters)) PositionalParameters.Clear(); else if (_inputParameters is not null) _inputParameters = null; // We're pointing at a user's NpgsqlParameterCollection Debug.Assert(_inputParameters is null || _inputParameters.Count == 0); Debug.Assert(_ownedInputParameters is null || _ownedInputParameters.Count == 0); } internal void ApplyCommandComplete(CommandCompleteMessage msg) { StatementType = msg.StatementType; Rows = msg.Rows; OID = msg.OID; } internal void ResetPreparation() => ConnectorPreparedOn = null; internal void PopulateOutputParameters(NpgsqlDataReader reader, ILogger logger) { Debug.Assert(_parameters is not null); var parameters = _parameters; var fieldCount = reader.FieldCount; switch (parameters.PlaceholderType) { case PlaceholderType.Mixed: case PlaceholderType.Named: { // In the case of named and mixed parameters we first try to populate all parameters with a named column match. // For backwards compat we allow populating named parameters as long as they haven't been filled yet. // So for every column that we couldn't match by name we fill the first output direction parameter that wasn't filled previously. // This means a row like {"a" => 1, "some_field" => 2} will populate the following output db params {"a" => 1, "b" => 2}. // And a row like {"some_field" => 1, "a" => 2} will populate them as follows {"a" => 2, "b" => 1}. var parameterIndices = new ArraySegment(ArrayPool.Shared.Rent(fieldCount), 0, fieldCount); var secondPassOrdinal = -1; for (var ordinal = 0; ordinal < fieldCount; ordinal++) { var name = reader.GetName(ordinal); var i = parameters.IndexOf(name); if (i is not -1 && parameters[i] is { IsOutputDirection: true } parameter) { SetValue(reader, logger, parameter, ordinal, i); parameterIndices[ordinal] = i; } else { parameterIndices[ordinal] = -1; if (secondPassOrdinal is -1) secondPassOrdinal = ordinal; } } if (secondPassOrdinal is -1) { ArrayPool.Shared.Return(parameterIndices.Array!); break; } // This set will also contain -1, but that's not a valid index so we can ignore it is included. var matchedParameters = new HashSet(parameterIndices); var parameterList = parameters.InternalList; for (var i = 0; i < parameterList.Count; i++) { // Find an output parameter that wasn't matched by name. if (parameterList[i] is not { IsOutputDirection: true } parameter || matchedParameters.Contains(i)) continue; SetValue(reader, logger, parameter, secondPassOrdinal, i); // And find the next unhandled ordinal. secondPassOrdinal = NextSecondPassOrdinal(parameterIndices, secondPassOrdinal); if (secondPassOrdinal is -1) break; } ArrayPool.Shared.Return(parameterIndices.Array!); break; static int NextSecondPassOrdinal(ArraySegment indices, int offset) { for (var i = offset + 1; i < indices.Count; i++) { if (indices[i] is -1) return i; } return -1; } } case PlaceholderType.Positional: { var parameterList = parameters.InternalList; var ordinal = 0; for (var i = 0; i < parameterList.Count; i++) { if (parameterList[i] is not { IsOutputDirection: true } parameter) continue; SetValue(reader, logger, parameter, ordinal, i); ordinal++; if (ordinal == fieldCount) break; } break; } } static void SetValue(NpgsqlDataReader reader, ILogger logger, NpgsqlParameter p, int ordinal, int index) { try { p.SetOutputValue(reader, ordinal); } catch (Exception ex) { logger.LogDebug(ex, "Failed to set value on output parameter instance '{ParameterNameOrIndex}' for output parameter {OutputName}", p.ParameterName is NpgsqlParameter.PositionalName ? index : p.ParameterName, reader.GetName(ordinal)); throw; } } } /// /// Returns the . /// public override string ToString() => CommandText; }
X Tutup