Skip to content

Commit 5f47620

Browse files
olavloiteskuruppu
andauthored
feat: run and runStream can return query stats (#857)
* feat: run and runStream can return query stats * fix: use const enum * fix: add missing semicolon Co-authored-by: skuruppu <skuruppu@google.com>
1 parent 772a12a commit 5f47620

4 files changed

Lines changed: 115 additions & 6 deletions

File tree

handwritten/spanner/src/database.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import {
5353
ExecuteSqlRequest,
5454
RunUpdateCallback,
5555
RunResponse,
56+
RunCallback,
5657
} from './transaction';
5758
import {
5859
AsyncRunTransactionCallback,
@@ -112,7 +113,6 @@ export type UpdateSchemaResponse = [
112113

113114
type PoolRequestCallback = RequestCallback<Session>;
114115

115-
type RunCallback = RequestCallback<Row[]>;
116116
type ResultSetStats = spannerClient.spanner.v1.ResultSetStats;
117117

118118
type GetSessionsOptions = PagedRequest<google.spanner.v1.IListSessionsRequest>;
@@ -1553,6 +1553,7 @@ class Database extends GrpcServiceObject {
15531553
optionsOrCallback?: TimestampBounds | RunCallback,
15541554
cb?: RunCallback
15551555
): void | Promise<RunResponse> {
1556+
let stats: ResultSetStats;
15561557
const rows: Row[] = [];
15571558
const callback =
15581559
typeof optionsOrCallback === 'function'
@@ -1565,11 +1566,12 @@ class Database extends GrpcServiceObject {
15651566

15661567
this.runStream(query, options)
15671568
.on('error', callback!)
1569+
.on('stats', _stats => (stats = _stats))
15681570
.on('data', row => {
15691571
rows.push(row);
15701572
})
15711573
.on('end', () => {
1572-
callback!(null, rows);
1574+
callback!(null, rows, stats);
15731575
});
15741576
}
15751577
runPartitionedUpdate(query: string | ExecuteSqlRequest): Promise<[number]>;
@@ -1783,6 +1785,7 @@ class Database extends GrpcServiceObject {
17831785
snapshot.end();
17841786
}
17851787
})
1788+
.on('stats', stats => proxyStream.emit('stats', stats))
17861789
.once('end', endListener)
17871790
.pipe(proxyStream);
17881791
});

handwritten/spanner/src/v1/spanner_client.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ declare namespace SpannerClient {
185185
(error: null | ServiceError, response: ExecuteBatchDmlResponse): void;
186186
}
187187

188-
enum QueryMode {
188+
const enum QueryMode {
189189
NORMAL,
190190
PLAN,
191191
PROFILE

handwritten/spanner/test/mockserver/mockspanner.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import ExecuteBatchDmlResponse = google.spanner.v1.ExecuteBatchDmlResponse;
2626
import ResultSet = google.spanner.v1.ResultSet;
2727
import Status = google.rpc.Status;
2828
import Any = google.protobuf.Any;
29+
import QueryMode = google.spanner.v1.ExecuteSqlRequest.QueryMode;
2930

3031
const PROTO_PATH = 'spanner.proto';
3132
const IMPORT_PATH = __dirname + '/../../../protos';
@@ -474,7 +475,8 @@ export class MockSpanner {
474475
switch (res.type) {
475476
case StatementResultType.RESULT_SET:
476477
const partialResultSets = MockSpanner.toPartialResultSets(
477-
res.resultSet
478+
res.resultSet,
479+
call.request.queryMode
478480
);
479481
// Resume on the next index after the last one seen by the client.
480482
const resumeIndex =
@@ -527,9 +529,13 @@ export class MockSpanner {
527529
/**
528530
* Splits a ResultSet into one PartialResultSet per row. This ensure that we can also test returning multiple partial results sets from a streaming method.
529531
* @param resultSet The ResultSet to split.
532+
* @param queryMode The query mode that was used to execute the query.
530533
*/
531534
private static toPartialResultSets(
532-
resultSet: protobuf.ResultSet
535+
resultSet: protobuf.ResultSet,
536+
queryMode:
537+
| google.spanner.v1.ExecuteSqlRequest.QueryMode
538+
| keyof typeof google.spanner.v1.ExecuteSqlRequest.QueryMode
533539
): protobuf.PartialResultSet[] {
534540
const res: protobuf.PartialResultSet[] = [];
535541
let first = true;
@@ -545,6 +551,13 @@ export class MockSpanner {
545551
}
546552
res.push(partial);
547553
}
554+
if (queryMode === QueryMode.PROFILE || queryMode === 'PROFILE') {
555+
// Include an empty query plan and statistics.
556+
res[res.length - 1].stats = {
557+
queryStats: {fields: {}},
558+
queryPlan: {planNodes: []},
559+
};
560+
}
548561
return res;
549562
}
550563

handwritten/spanner/test/spanner.ts

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,14 @@ import {
3838
SessionPoolExhaustedError,
3939
SessionPoolOptions,
4040
} from '../src/session-pool';
41-
import CreateInstanceMetadata = google.spanner.admin.instance.v1.CreateInstanceMetadata;
4241
import {Json} from '../src/codec';
42+
import CreateInstanceMetadata = google.spanner.admin.instance.v1.CreateInstanceMetadata;
4343
import Done = Mocha.Done;
4444
import QueryOptions = google.spanner.v1.ExecuteSqlRequest.QueryOptions;
4545
import v1 = google.spanner.v1;
4646
import IQueryOptions = google.spanner.v1.ExecuteSqlRequest.IQueryOptions;
47+
import ResultSetStats = google.spanner.v1.ResultSetStats;
48+
import {SpannerClient as s} from '../src/v1';
4749

4850
function numberToEnglishWord(num: number): string {
4951
switch (num) {
@@ -172,6 +174,97 @@ describe('Spanner with mock server', () => {
172174
}
173175
});
174176

177+
it('should return statistics', async () => {
178+
const database = newTestDatabase();
179+
try {
180+
const [rows, stats] = await database.run({
181+
sql: selectSql,
182+
queryMode: s.QueryMode.PROFILE,
183+
});
184+
assert.strictEqual(rows.length, 3);
185+
assert.ok(stats);
186+
assert.ok(stats.queryPlan);
187+
} finally {
188+
await database.close();
189+
}
190+
});
191+
192+
it('should return statistics from snapshot', async () => {
193+
const database = newTestDatabase();
194+
try {
195+
const [snapshot] = await database.getSnapshot();
196+
const [rows, stats] = await snapshot.run({
197+
sql: selectSql,
198+
queryMode: s.QueryMode.PROFILE,
199+
});
200+
assert.strictEqual(rows.length, 3);
201+
assert.ok(stats);
202+
assert.ok(stats.queryPlan);
203+
snapshot.end();
204+
} finally {
205+
await database.close();
206+
}
207+
});
208+
209+
it('should emit query statistics', done => {
210+
const database = newTestDatabase();
211+
let rowCount = 0;
212+
let stats: ResultSetStats;
213+
database
214+
.runStream({
215+
sql: selectSql,
216+
queryMode: s.QueryMode.PROFILE,
217+
})
218+
.on('data', () => rowCount++)
219+
.on('stats', _stats => (stats = _stats))
220+
.on('end', () => {
221+
assert.strictEqual(rowCount, 3);
222+
assert.ok(stats);
223+
assert.ok(stats.queryPlan);
224+
database.close().then(() => done());
225+
});
226+
});
227+
228+
it('should emit query statistics from snapshot', done => {
229+
const database = newTestDatabase();
230+
let rowCount = 0;
231+
let stats: ResultSetStats;
232+
database.getSnapshot().then(response => {
233+
const [snapshot] = response;
234+
snapshot
235+
.runStream({
236+
sql: selectSql,
237+
queryMode: s.QueryMode.PROFILE,
238+
})
239+
.on('data', () => rowCount++)
240+
.on('stats', _stats => (stats = _stats))
241+
.on('end', () => {
242+
assert.strictEqual(rowCount, 3);
243+
assert.ok(stats);
244+
assert.ok(stats.queryPlan);
245+
snapshot.end();
246+
database.close().then(() => done());
247+
});
248+
});
249+
});
250+
251+
it('should call callback with statistics', done => {
252+
const database = newTestDatabase();
253+
database.run(
254+
{
255+
sql: selectSql,
256+
queryMode: s.QueryMode.PROFILE,
257+
},
258+
(err, rows, stats) => {
259+
assert.ifError(err);
260+
assert.strictEqual(rows.length, 3);
261+
assert.ok(stats);
262+
assert.ok(stats.queryPlan);
263+
database.close().then(() => done());
264+
}
265+
);
266+
});
267+
175268
it('should execute update', async () => {
176269
const update = {
177270
sql: insertSql,

0 commit comments

Comments
 (0)