|
1 | 1 | using System; |
| 2 | +using System.Buffers; |
2 | 3 | using System.Collections.Generic; |
3 | 4 | using System.Data; |
4 | 5 | using System.Data.Common; |
5 | 6 | using System.Diagnostics; |
6 | 7 | using System.Diagnostics.CodeAnalysis; |
7 | 8 | using System.Runtime.CompilerServices; |
| 9 | +using Microsoft.Extensions.Logging; |
8 | 10 | using Npgsql.BackendMessages; |
9 | 11 | using Npgsql.Internal; |
10 | 12 |
|
@@ -41,6 +43,7 @@ public override string CommandText |
41 | 43 | /// <inheritdoc cref="DbBatchCommand.Parameters"/> |
42 | 44 | public new NpgsqlParameterCollection Parameters => _parameters ??= []; |
43 | 45 |
|
| 46 | + internal bool HasOutputParameters => _parameters?.HasOutputParameters == true; |
44 | 47 |
|
45 | 48 | /// <inheritdoc/> |
46 | 49 | public override NpgsqlParameter CreateParameter() => new(); |
@@ -273,6 +276,112 @@ internal void ApplyCommandComplete(CommandCompleteMessage msg) |
273 | 276 |
|
274 | 277 | internal void ResetPreparation() => ConnectorPreparedOn = null; |
275 | 278 |
|
| 279 | + internal void PopulateOutputParameters(NpgsqlDataReader reader, ILogger logger) |
| 280 | + { |
| 281 | + Debug.Assert(_parameters is not null); |
| 282 | + var parameters = _parameters; |
| 283 | + var fieldCount = reader.FieldCount; |
| 284 | + switch (parameters.PlaceholderType) |
| 285 | + { |
| 286 | + case PlaceholderType.Mixed: |
| 287 | + case PlaceholderType.Named: |
| 288 | + { |
| 289 | + // In the case of named and mixed parameters we first try to populate all parameters with a named column match. |
| 290 | + // For backwards compat we allow populating named parameters as long as they haven't been filled yet. |
| 291 | + // So for every column that we couldn't match by name we fill the first output direction parameter that wasn't filled previously. |
| 292 | + // This means a row like {"a" => 1, "some_field" => 2} will populate the following output db params {"a" => 1, "b" => 2}. |
| 293 | + // And a row like {"some_field" => 1, "a" => 2} will populate them as follows {"a" => 2, "b" => 1}. |
| 294 | + |
| 295 | + var parameterIndices = new ArraySegment<int>(ArrayPool<int>.Shared.Rent(fieldCount), 0, fieldCount); |
| 296 | + var secondPassOrdinal = -1; |
| 297 | + for (var ordinal = 0; ordinal < fieldCount; ordinal++) |
| 298 | + { |
| 299 | + var name = reader.GetName(ordinal); |
| 300 | + var i = parameters.IndexOf(name); |
| 301 | + if (i is not -1 && parameters[i] is { IsOutputDirection: true } parameter) |
| 302 | + { |
| 303 | + SetValue(reader, logger, parameter, ordinal, i); |
| 304 | + parameterIndices[ordinal] = i; |
| 305 | + } |
| 306 | + else |
| 307 | + { |
| 308 | + parameterIndices[ordinal] = -1; |
| 309 | + if (secondPassOrdinal is -1) |
| 310 | + secondPassOrdinal = ordinal; |
| 311 | + } |
| 312 | + } |
| 313 | + |
| 314 | + if (secondPassOrdinal is -1) |
| 315 | + { |
| 316 | + ArrayPool<int>.Shared.Return(parameterIndices.Array!); |
| 317 | + break; |
| 318 | + } |
| 319 | + |
| 320 | + // This set will also contain -1, but that's not a valid index so we can ignore it is included. |
| 321 | + var matchedParameters = new HashSet<int>(parameterIndices); |
| 322 | + var parameterList = parameters.InternalList; |
| 323 | + for (var i = 0; i < parameterList.Count; i++) |
| 324 | + { |
| 325 | + // Find an output parameter that wasn't matched by name. |
| 326 | + if (parameterList[i] is not { IsOutputDirection: true } parameter || matchedParameters.Contains(i)) |
| 327 | + continue; |
| 328 | + |
| 329 | + SetValue(reader, logger, parameter, secondPassOrdinal, i); |
| 330 | + |
| 331 | + // And find the next unhandled ordinal. |
| 332 | + secondPassOrdinal = NextSecondPassOrdinal(parameterIndices, secondPassOrdinal); |
| 333 | + if (secondPassOrdinal is -1) |
| 334 | + break; |
| 335 | + } |
| 336 | + |
| 337 | + ArrayPool<int>.Shared.Return(parameterIndices.Array!); |
| 338 | + break; |
| 339 | + |
| 340 | + static int NextSecondPassOrdinal(ArraySegment<int> indices, int offset) |
| 341 | + { |
| 342 | + for (var i = offset + 1; i < indices.Count; i++) |
| 343 | + { |
| 344 | + if (indices[i] is -1) |
| 345 | + return i; |
| 346 | + } |
| 347 | + |
| 348 | + return -1; |
| 349 | + } |
| 350 | + } |
| 351 | + case PlaceholderType.Positional: |
| 352 | + { |
| 353 | + var parameterList = parameters.InternalList; |
| 354 | + var ordinal = 0; |
| 355 | + for (var i = 0; i < parameterList.Count; i++) |
| 356 | + { |
| 357 | + if (parameterList[i] is not { IsOutputDirection: true } parameter) |
| 358 | + continue; |
| 359 | + |
| 360 | + SetValue(reader, logger, parameter, ordinal, i); |
| 361 | + |
| 362 | + ordinal++; |
| 363 | + if (ordinal == fieldCount) |
| 364 | + break; |
| 365 | + } |
| 366 | + break; |
| 367 | + } |
| 368 | + } |
| 369 | + |
| 370 | + static void SetValue(NpgsqlDataReader reader, ILogger logger, NpgsqlParameter p, int ordinal, int index) |
| 371 | + { |
| 372 | + try |
| 373 | + { |
| 374 | + p.SetOutputValue(reader, ordinal); |
| 375 | + } |
| 376 | + catch (Exception ex) |
| 377 | + { |
| 378 | + logger.LogDebug(ex, "Failed to set value on output parameter instance '{ParameterNameOrIndex}' for output parameter {OutputName}", |
| 379 | + p.ParameterName is NpgsqlParameter.PositionalName ? index : p.ParameterName, reader.GetName(ordinal)); |
| 380 | + throw; |
| 381 | + } |
| 382 | + } |
| 383 | + } |
| 384 | + |
276 | 385 | /// <summary> |
277 | 386 | /// Returns the <see cref="CommandText"/>. |
278 | 387 | /// </summary> |
|
0 commit comments