using Google.Protobuf;
using AVEVA.Historian.Client.Models;
using AVEVA.Historian.Client.Protocol;
using AVEVA.Historian.Client.Wcf;
using GrpcRetrieval = ArchestrA.Grpc.Contract.Retrieval;
namespace AVEVA.Historian.Client.Grpc;
///
/// Executes SQL commands over the 2023 R2 gRPC transport (HCAL R1.1), mirroring
/// 's two-op ExeC/GetR flow. The 2020 WCF path uses a
/// dedicated GetRecordSetByteStream op; the gRPC front door has no such RPC, so the NRBF
/// recordset stream would be fetched through the generic RetrievalService.GetNextQueryResultBuffer
/// keyed by the query handle ExecuteSqlCommand returns. ExecuteSqlCommand takes the
/// uppercase string session handle; the result-buffer fetch takes the transient uint client
/// handle (both come from the one Open2 session).
///
/// SERVER-WALLED (captured 2026-06-22). The 2023 R2 front-door
/// RetrievalService.ExecuteSqlCommand faults server-side before returning a query handle:
/// the response carries native error 38 wrapping a managed
/// System.IndexOutOfRangeException ... at aahClientAccessPoint.CSrvDbConnection.ExecuteSqlCommand.
/// This is a server-side CSrvDbConnection (SQL DB-connection) precondition that the pure
/// managed gRPC session does not establish — the same class of wall as
/// StorageService.OpenStorageConnection. Priming Retr.GetV does not clear it, and
/// a HistoryService.RegisterTags prime does NOT clear it either (tried live 2026-06-22 on
/// both read-only 0x402 and write-enabled 0x401 sessions: RegisterTags itself
/// returned false and ExecuteSqlCommand faulted with the identical native-38 IndexOutOfRange) —
/// so unlike the OpenStorageConnection wall, the SQL DB-connection context is not established by the
/// RegisterTags family. The request framing here is the captured/expected shape; the op stays bounded
/// behind until the DB-connection registration is
/// reproduced. Use the WCF transport for SQL.
///
///
internal static class HistorianGrpcSqlClient
{
// GetNextQueryResultBuffer is byte-stream-paged; a small record set returns in one page. Runaway guard.
private const int MaxPages = 4096;
public static Task ExecuteSqlCommandAsync(
HistorianClientOptions options,
string command,
HistorianSqlExecuteOption option,
CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrWhiteSpace(command);
return Task.Run(() => ExecuteSqlCommand(options, command, option, cancellationToken), cancellationToken);
}
private static HistorianSqlResult ExecuteSqlCommand(
HistorianClientOptions options,
string command,
HistorianSqlExecuteOption option,
CancellationToken cancellationToken)
{
using HistorianGrpcConnection connection = HistorianGrpcChannelFactory.Create(options);
HistorianGrpcHandshake.Session session = HistorianGrpcHandshake.OpenSession(connection, options, cancellationToken);
var retrievalClient = new GrpcRetrieval.RetrievalService.RetrievalServiceClient(connection.Channel);
DateTime Deadline() => DateTime.UtcNow.Add(options.RequestTimeout);
// Prime the Retrieval service version handshake (Retr.GetV) before the string-handle SQL op, as
// the native WCF SQL path does — the server-side ExecuteSqlCommand otherwise faults.
retrievalClient.GetRetrievalInterfaceVersion(
new GrpcRetrieval.GetRetrievalInterfaceVersionRequest(), connection.Metadata, Deadline(), cancellationToken);
GrpcRetrieval.ExecuteSqlCommandResponse exec = retrievalClient.ExecuteSqlCommand(
new GrpcRetrieval.ExecuteSqlCommandRequest
{
StrHandle = session.StringHandle,
StrCommand = command,
UiOption = (uint)option,
UiQueryHandle = 0
},
connection.Metadata,
Deadline(),
cancellationToken);
if (!(exec.Status?.BSuccess ?? false))
{
// Captured 2026-06-22: the server-side CSrvDbConnection.ExecuteSqlCommand throws
// IndexOutOfRange (native error 38) — a DB-connection precondition the pure managed gRPC
// session doesn't establish. Surface the SDK's evidence-missing signal rather than a raw
// server fault. See the class remarks.
throw new ProtocolEvidenceMissingException(
"ExecuteSqlCommand over gRPC: server-side CSrvDbConnection.ExecuteSqlCommand faults " +
"(IndexOutOfRange / native error 38) — an unmet DB-connection precondition (gRPC transport). Use WCF.");
}
int returnValue = exec.IRetValue;
uint queryHandle = exec.UiQueryHandle;
using MemoryStream accumulated = new();
for (int page = 0; page < MaxPages; page++)
{
cancellationToken.ThrowIfCancellationRequested();
GrpcRetrieval.GetNextQueryResultBufferResponse buffer = retrievalClient.GetNextQueryResultBuffer(
new GrpcRetrieval.GetNextQueryResultBufferRequest { UiHandle = session.ClientHandle, UiQueryHandle = queryHandle },
connection.Metadata,
Deadline(),
cancellationToken);
byte[] resultBuffer = buffer.BtQueryResult?.ToByteArray() ?? [];
// GetR is false-even-on-success: the final page returns false with the data still in the
// buffer, so always consume the buffer first, then stop on a false status or an empty page.
if (resultBuffer.Length > 0)
{
accumulated.Write(resultBuffer, 0, resultBuffer.Length);
}
if (!(buffer.Status?.BSuccess ?? false) || resultBuffer.Length == 0)
{
break;
}
}
return HistorianSqlResultProtocol.Parse(accumulated.ToArray(), returnValue);
}
}