Skip to content

Commit 0fcd03e

Browse files
committed
feat(sea): surface numModifiedRows on the async (runAsync) path
The sync path already reports numModifiedRows for SEA DML; the async (submitStatement) path returned all-null because readRichStatusFields short-circuited for asyncStatement (the rich accessors used to live only on the terminal sync Statement). Kernel 75bfa52 now derives a DML's count off the terminal GetStatement poll the async path already makes (no extra fetch, gated so SELECTs are untouched) and exposes the four extended-status accessors on the napi AsyncStatement. This change reads them off the async handle: - readRichStatusFields now reads off this.asyncStatement (extracted into a shared readStatusFieldsFrom used by both the sync Statement and the async AsyncStatement); - status() surfaces the fields on the async Succeeded state (the same GetStatement poll it just made carried them) — without materialising the result, so async streaming is unaffected. Bumps KERNEL_REV to 75bfa52 and updates the napi contract (native/sea/index.d.ts) with the new AsyncStatement accessors. Verified live (pecotesting http_path2): SEA async INSERT/UPDATE/DELETE/ MERGE report numModifiedRows 3/2/1/2 — matching the sync path and Thrift — with fetchAll() unchanged and SELECT carrying no count. Co-authored-by: Isaac
1 parent e202af3 commit 0fcd03e

4 files changed

Lines changed: 109 additions & 14 deletions

File tree

KERNEL_REV

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
81c69078f8cbc59391824887a7d4be666ece9510
1+
75bfa526cf70c6c37d565e3cbb19c6d922714174

lib/sea/SeaOperationBackend.ts

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -411,10 +411,15 @@ export default class SeaOperationBackend implements IOperationBackend {
411411
if (this.asyncStatement) {
412412
// Async query path: report the real kernel state (single
413413
// GetStatementStatus RPC — no polling here; `waitUntilReady` owns the
414-
// poll loop). The rich status fields (`numModifiedRows` etc.) live on the
415-
// terminal sync `Statement`, which the async path never produces, so they
416-
// stay undefined here.
414+
// poll loop). On a terminal Succeeded state, that same GetStatement poll
415+
// carried the rich status fields (the kernel derives a DML's
416+
// `numModifiedRows` from the inline count on the terminal response), so
417+
// surface them — mirroring the sync path. Reading them does not fetch the
418+
// result.
417419
const state = statusStringToOperationState(await this.asyncStatement.status());
420+
if (state === OperationState.Succeeded) {
421+
return { state, hasResultSet: true, ...(await this.readRichStatusFields()) };
422+
}
418423
return { state, hasResultSet: true };
419424
}
420425
if (this.cancellableExecution) {
@@ -496,12 +501,16 @@ export default class SeaOperationBackend implements IOperationBackend {
496501
errorDetailsJson: null,
497502
};
498503

499-
// The async path never produces a terminal sync `Statement`, so there is
500-
// nothing to read these off of. (The constructor guarantees exactly one of
501-
// `asyncStatement` / `statement` / `cancellableExecution`, so `asyncStatement`
502-
// being set already implies `cancellableExecution` is undefined.)
504+
// Async (submit/poll) path: a DML's `numModifiedRows` rides in the result
505+
// set, which SEA delivers on the terminal `GetStatement` poll. The kernel
506+
// derives it off that poll (no extra fetch) and exposes it on the
507+
// `AsyncStatement`'s status accessors — so read them directly. The value is
508+
// populated once the statement has reached a terminal state via
509+
// `status()` / `waitUntilReady` (which polled it); it stays null before
510+
// that and for SELECTs. Reading does NOT force a result materialisation, so
511+
// async streaming is untouched.
503512
if (this.asyncStatement) {
504-
return empty;
513+
return this.readStatusFieldsFrom(this.asyncStatement);
505514
}
506515

507516
// Ensure the sync path's blocking `result()` has resolved and stored the
@@ -517,13 +526,31 @@ export default class SeaOperationBackend implements IOperationBackend {
517526
}
518527
}
519528

520-
const handle = this.blockingStatement as Partial<SeaStatusFieldsHandle> | undefined;
521-
if (!handle || typeof handle.numModifiedRows !== 'function') {
522-
// No resolved statement, or a binding that predates the rich-field
523-
// accessors — degrade to all-null.
529+
return this.readStatusFieldsFrom(this.blockingStatement);
530+
}
531+
532+
/**
533+
* Read the four rich-status accessors (`numModifiedRows` / `displayMessage` /
534+
* `diagnosticInfo` / `errorDetailsJson`) off a kernel handle — the terminal
535+
* sync `Statement` or the async `AsyncStatement`, which expose the same
536+
* accessor shape. Per-field read errors are swallowed to `null`: a failed
537+
* status-field read must never turn a successful operation's status query
538+
* into a throw. Degrades to all-null for a missing handle or a binding that
539+
* predates the accessors.
540+
*/
541+
private async readStatusFieldsFrom(handle: unknown): Promise<SeaRichStatusFields> {
542+
const empty: SeaRichStatusFields = {
543+
numModifiedRows: null,
544+
displayMessage: null,
545+
diagnosticInfo: null,
546+
errorDetailsJson: null,
547+
};
548+
549+
const candidate = handle as Partial<SeaStatusFieldsHandle> | undefined;
550+
if (!candidate || typeof candidate.numModifiedRows !== 'function') {
524551
return empty;
525552
}
526-
const richHandle = handle as SeaStatusFieldsHandle;
553+
const richHandle = candidate as SeaStatusFieldsHandle;
527554

528555
const readOrNull = async <T>(read: () => Promise<T | null>): Promise<T | null> => {
529556
try {

native/sea/index.d.ts

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/unit/sea/execution.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,33 @@ class FakeAsyncStatement {
148148
public async close(): Promise<void> {
149149
this.closed = true;
150150
}
151+
152+
// Extended status accessors exposed on the napi AsyncStatement. The kernel
153+
// populates them off the terminal GetStatement poll (a DML's count rides on
154+
// that response); configurable here to assert the driver surfaces them
155+
// through op.status() on the async path.
156+
public rich: SeaRichStatusValues = {
157+
numModifiedRows: null,
158+
displayMessage: null,
159+
diagnosticInfo: null,
160+
errorDetailsJson: null,
161+
};
162+
163+
public async numModifiedRows(): Promise<number | null> {
164+
return this.rich.numModifiedRows;
165+
}
166+
167+
public async displayMessage(): Promise<string | null> {
168+
return this.rich.displayMessage;
169+
}
170+
171+
public async diagnosticInfo(): Promise<string | null> {
172+
return this.rich.diagnosticInfo;
173+
}
174+
175+
public async errorDetailsJson(): Promise<string | null> {
176+
return this.rich.errorDetailsJson;
177+
}
151178
}
152179

153180
/**
@@ -942,6 +969,36 @@ describe('SeaOperationBackend — async (submitStatement) path', () => {
942969
expect((await ok.status(false)).state).to.equal(OperationState.Succeeded);
943970
});
944971

972+
it('surfaces rich-status fields (numModifiedRows etc.) through op.status() once Succeeded', async () => {
973+
// The kernel derives a DML's count off the terminal GetStatement poll and
974+
// exposes it on the AsyncStatement accessors; the backend surfaces it on
975+
// op.status() at the Succeeded state — parity with the sync path.
976+
const stmt = new FakeAsyncStatement('Succeeded');
977+
stmt.rich = {
978+
numModifiedRows: 7,
979+
displayMessage: 'UPDATE 0 7',
980+
diagnosticInfo: 'ok',
981+
errorDetailsJson: null,
982+
};
983+
const op = makeAsyncOp(stmt);
984+
const status = await op.status(false);
985+
expect(status.state).to.equal(OperationState.Succeeded);
986+
expect(status.numModifiedRows).to.equal(7);
987+
expect(status.displayMessage).to.equal('UPDATE 0 7');
988+
expect(status.diagnosticInfo).to.equal('ok');
989+
expect(status.errorDetailsJson).to.equal(null);
990+
});
991+
992+
it('does not read rich-status fields before the async statement is terminal', async () => {
993+
// A Running async statement must not surface (or fabricate) a count.
994+
const stmt = new FakeAsyncStatement('Running');
995+
stmt.rich = { numModifiedRows: 99, displayMessage: null, diagnosticInfo: null, errorDetailsJson: null };
996+
const op = makeAsyncOp(stmt);
997+
const status = await op.status(false);
998+
expect(status.state).to.equal(OperationState.Running);
999+
expect(status.numModifiedRows).to.equal(undefined);
1000+
});
1001+
9451002
it('waitUntilReady() polls status() until terminal, firing the progress callback each tick', async () => {
9461003
const stmt = new FakeAsyncStatement(['Pending', 'Running', 'Succeeded']);
9471004
const op = makeAsyncOp(stmt);

0 commit comments

Comments
 (0)