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); } }