@@ -85,6 +85,33 @@ export type SeaOperationStatement = SeaStatementHandle & Partial<SeaStatement>;
8585 */
8686type SeaFetchHandle = Pick < SeaStatement , 'fetchNextBatch' | 'schema' > ;
8787
88+ /**
89+ * The rich operation-status surface the kernel exposes on a terminal sync
90+ * `Statement` (`numModifiedRows` / `displayMessage` / `diagnosticInfo` /
91+ * `errorDetailsJson`). These accessors live ONLY on the blocking `Statement`
92+ * (metadata path + sync `runAsync:false` path once `result()` resolves) — the
93+ * async `AsyncStatement` / `AsyncResultHandle` do not expose them — so the
94+ * reader below is best-effort and returns an empty record when the handle
95+ * predates this surface or the operation never resolved to a `Statement`.
96+ */
97+ type SeaStatusFieldsHandle = Pick <
98+ SeaStatement ,
99+ 'numModifiedRows' | 'displayMessage' | 'diagnosticInfo' | 'errorDetailsJson'
100+ > ;
101+
102+ /**
103+ * The rich operation-status fields, as the kernel returns them (each `null`
104+ * when the server didn't supply it — e.g. `numModifiedRows` is null for a
105+ * SELECT). Carried onto the neutral `OperationStatus` and ultimately into the
106+ * Thrift `TGetOperationStatusResp` so SEA reports parity with the Thrift path.
107+ */
108+ interface SeaRichStatusFields {
109+ numModifiedRows : number | null ;
110+ displayMessage : string | null ;
111+ diagnosticInfo : string | null ;
112+ errorDetailsJson : string | null ;
113+ }
114+
88115/** Poll cadence for the async `status()` loop — matches the Thrift backend's 100ms. */
89116const STATUS_POLL_INTERVAL_MS = 100 ;
90117
@@ -377,7 +404,9 @@ export default class SeaOperationBackend implements IOperationBackend {
377404 if ( this . asyncStatement ) {
378405 // Async query path: report the real kernel state (single
379406 // GetStatementStatus RPC — no polling here; `waitUntilReady` owns the
380- // poll loop).
407+ // poll loop). The rich status fields (`numModifiedRows` etc.) live on the
408+ // terminal sync `Statement`, which the async path never produces, so they
409+ // stay undefined here.
381410 const state = statusStringToOperationState ( await this . asyncStatement . status ( ) ) ;
382411 return { state, hasResultSet : true } ;
383412 }
@@ -386,11 +415,16 @@ export default class SeaOperationBackend implements IOperationBackend {
386415 // server-side; there is no per-status RPC to query while it runs. Report
387416 // Running until `result()` has materialised the terminal statement, then
388417 // Succeeded — mirroring the kernel's blocking-then-terminal lifecycle.
389- const state = this . fetchHandlePromise ? OperationState . Succeeded : OperationState . Running ;
390- return { state, hasResultSet : true } ;
418+ if ( ! this . fetchHandlePromise ) {
419+ return { state : OperationState . Running , hasResultSet : true } ;
420+ }
421+ // The blocking `result()` has resolved a terminal `Statement` — surface
422+ // its rich status fields alongside the Succeeded state.
423+ return { state : OperationState . Succeeded , hasResultSet : true , ...( await this . readRichStatusFields ( ) ) } ;
391424 }
392- // Metadata path: the kernel statement is already terminal.
393- return { state : OperationState . Succeeded , hasResultSet : true } ;
425+ // Metadata path: the kernel statement is already terminal — read its rich
426+ // fields too (they are `null` for metadata results, by design).
427+ return { state : OperationState . Succeeded , hasResultSet : true , ...( await this . readRichStatusFields ( ) ) } ;
394428 }
395429
396430 public async waitUntilReady ( options ?: IOperationBackendWaitOptions ) : Promise < void > {
@@ -402,8 +436,11 @@ export default class SeaOperationBackend implements IOperationBackend {
402436 }
403437 // Metadata path: the kernel statement has already resolved, so there is
404438 // nothing to poll. seaFinished fires the progress callback once with a
405- // synthesised completion tick, matching the Thrift path's final tick.
406- return seaFinished ( this . lifecycle , options ) ;
439+ // synthesised completion tick, matching the Thrift path's final tick. The
440+ // rich-field reader is passed lazily so it only runs when a callback is
441+ // wired (metadata statements report all-null, but the surface stays
442+ // consistent with the query paths).
443+ return seaFinished ( this . lifecycle , options , ( ) => this . readRichStatusFields ( ) ) ;
407444 }
408445
409446 public async cancel ( ) : Promise < Status > {
@@ -418,6 +455,85 @@ export default class SeaOperationBackend implements IOperationBackend {
418455 // Internals.
419456 // ---------------------------------------------------------------------------
420457
458+ /**
459+ * Read the kernel's rich operation-status fields (`numModifiedRows` /
460+ * `displayMessage` / `diagnosticInfo` / `errorDetailsJson`) off the terminal
461+ * sync `Statement`. These accessors live only on the blocking `Statement`
462+ * (metadata path, or the sync `runAsync:false` path once `result()` has
463+ * resolved) — not on the async `AsyncStatement` / `AsyncResultHandle` — so:
464+ *
465+ * - on the async path we have no `Statement`, so we return all-null;
466+ * - on the sync path we await `getFetchHandle()` first, which both drives
467+ * `result()` to completion and stores the resolved `Statement` on
468+ * `blockingStatement` (the handle that backs the accessors);
469+ * - if the (older) binding predates these accessors we degrade to all-null
470+ * rather than throwing — `getOperationStatus()` must never fail just
471+ * because the rich fields are unavailable.
472+ *
473+ * Errors from the individual accessors are swallowed to null: a failed
474+ * status-field read must not turn a successful operation's status query into
475+ * a throw. The fields are best-effort metadata, not the operation outcome.
476+ */
477+ private async readRichStatusFields ( ) : Promise < SeaRichStatusFields > {
478+ const empty : SeaRichStatusFields = {
479+ numModifiedRows : null ,
480+ displayMessage : null ,
481+ diagnosticInfo : null ,
482+ errorDetailsJson : null ,
483+ } ;
484+
485+ // The async path never produces a terminal sync `Statement`, so there is
486+ // nothing to read these off of.
487+ if ( this . asyncStatement && ! this . cancellableExecution ) {
488+ return empty ;
489+ }
490+
491+ // Ensure the sync path's blocking `result()` has resolved and stored the
492+ // terminal `Statement` on `blockingStatement` (no-op on the metadata path,
493+ // where `blockingStatement` was set at construction).
494+ if ( this . cancellableExecution ) {
495+ try {
496+ await this . getFetchHandle ( ) ;
497+ } catch {
498+ // The operation failed/cancelled — its outcome surfaces through the
499+ // wait/fetch path; status-field reads have nothing to add.
500+ return empty ;
501+ }
502+ }
503+
504+ const handle = this . blockingStatement as Partial < SeaStatusFieldsHandle > | undefined ;
505+ if ( ! handle || typeof handle . numModifiedRows !== 'function' ) {
506+ // No resolved statement, or a binding that predates the rich-field
507+ // accessors — degrade to all-null.
508+ return empty ;
509+ }
510+ const richHandle = handle as SeaStatusFieldsHandle ;
511+
512+ const readOrNull = async < T > ( read : ( ) => Promise < T | null > ) : Promise < T | null > => {
513+ try {
514+ return await read ( ) ;
515+ } catch ( err ) {
516+ this . context
517+ . getLogger ( )
518+ . log (
519+ LogLevel . debug ,
520+ `SEA status-field read failed for operation ${ this . _id } ; reporting null. Cause: ` +
521+ `${ err instanceof Error ? err . message : String ( err ) } ` ,
522+ ) ;
523+ return null ;
524+ }
525+ } ;
526+
527+ const [ numModifiedRows , displayMessage , diagnosticInfo , errorDetailsJson ] = await Promise . all ( [
528+ readOrNull ( ( ) => richHandle . numModifiedRows ( ) ) ,
529+ readOrNull ( ( ) => richHandle . displayMessage ( ) ) ,
530+ readOrNull ( ( ) => richHandle . diagnosticInfo ( ) ) ,
531+ readOrNull ( ( ) => richHandle . errorDetailsJson ( ) ) ,
532+ ] ) ;
533+
534+ return { numModifiedRows, displayMessage, diagnosticInfo, errorDetailsJson } ;
535+ }
536+
421537 /**
422538 * Poll the kernel `AsyncStatement` to a terminal state on a fixed 100ms
423539 * cadence, mirroring the Thrift backend's `waitUntilReady` loop. We poll
@@ -547,9 +663,13 @@ export default class SeaOperationBackend implements IOperationBackend {
547663 // `getFetchHandle()` drives `result()` and memoises the resolved Statement
548664 // (also stored on `blockingStatement` so `close()` can reach it).
549665 await this . getFetchHandle ( ) ;
550- // Single completion tick, matching the metadata path.
666+ // Single completion tick, matching the metadata path — carrying the rich
667+ // status fields (numModifiedRows etc.) read off the now-terminal Statement.
551668 if ( options ?. callback ) {
552- await Promise . resolve ( options . callback ( { state : OperationState . Succeeded , hasResultSet : true } ) ) ;
669+ const richFields = await this . readRichStatusFields ( ) ;
670+ await Promise . resolve (
671+ options . callback ( { state : OperationState . Succeeded , hasResultSet : true , ...richFields } ) ,
672+ ) ;
553673 }
554674 }
555675
0 commit comments