Skip to content

Commit eaea44c

Browse files
authored
Auto-close cursors as appropriate
* Add concept of auto closing cursors * Auto cleanup cursors * Test cases for auto cleanup * Auto close statements run by the JobManager * Remove useless comment * Fix JobManager's use of query close * Correct close cursors whether prepared or not * Comment about multiple result tabs in the future * autoClose -> shouldAutoClose * shouldAutoClose to be solely boolean * Duble check if cursor is already closed
1 parent 09842af commit eaea44c

File tree

5 files changed

+98
-20
lines changed

5 files changed

+98
-20
lines changed

src/connection/manager.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,10 @@ export class SQLJobManager {
9494
// 2147483647 is NOT arbitrary. On the server side, this is processed as a Java
9595
// int. This is the largest number available without overflow (Integer.MAX_VALUE)
9696
const rowsToFetch = 2147483647;
97-
const results = await selected.job.query<T>(query, {parameters}).run(rowsToFetch);
97+
98+
const statement = selected.job.query<T>(query, {parameters});
99+
const results = await statement.run(rowsToFetch);
100+
statement.close();
98101
return results.data;
99102
} else {
100103
const instance = getInstance();

src/connection/query.ts

Lines changed: 55 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,28 +12,57 @@ export class Query<T> {
1212
private correlationId: string;
1313
private sql: string;
1414
private isPrepared: boolean = false;
15-
private parameters: any[];
15+
private parameters: any[]|undefined;
1616
private rowsToFetch: number = 100;
1717
private isCLCommand: boolean;
1818
private state: QueryState = QueryState.NOT_YET_RUN;
1919

20-
constructor(private job: SQLJob, query: string, opts: QueryOptions = {isClCommand: false, parameters: undefined}) {
20+
public shouldAutoClose: boolean;
21+
22+
constructor(private job: SQLJob, query: string, opts: QueryOptions = { isClCommand: false, parameters: undefined, autoClose: false }) {
2123
this.job = job;
2224
this.isPrepared = (undefined !== opts.parameters);
2325
this.parameters = opts.parameters;
2426
this.sql = query;
2527
this.isCLCommand = opts.isClCommand;
28+
this.shouldAutoClose = opts.autoClose;
29+
2630
Query.globalQueryList.push(this);
2731
}
32+
2833
public static byId(id: string) {
2934
return (undefined === id || '' === id) ? undefined : Query.globalQueryList.find(query => query.correlationId === id);
3035
}
36+
37+
public static getOpenIds(forJob?: string) {
38+
return this.globalQueryList
39+
.filter(q => q.job.id === forJob || forJob === undefined)
40+
.map(q => q.correlationId);
41+
}
42+
43+
public static async cleanup() {
44+
let closePromises = [];
45+
46+
// First, let's check to see if we should also cleanup
47+
// any cursors that remain open, and we've been told to close
48+
for (const query of this.globalQueryList) {
49+
if (query.shouldAutoClose) {
50+
closePromises.push(query.close())
51+
}
52+
};
53+
54+
await Promise.all(closePromises);
55+
56+
// Automatically remove any queries done and dusted. They're useless.
57+
this.globalQueryList = this.globalQueryList.filter(q => q.getState() !== QueryState.RUN_DONE);
58+
}
59+
3160
public async run(rowsToFetch: number = this.rowsToFetch): Promise<QueryResult<T>> {
3261
switch (this.state) {
33-
case QueryState.RUN_MORE_DATA_AVAILABLE:
34-
throw new Error('Statement has already been run');
35-
case QueryState.RUN_DONE:
36-
throw new Error('Statement has already been fully run');
62+
case QueryState.RUN_MORE_DATA_AVAILABLE:
63+
throw new Error('Statement has already been run');
64+
case QueryState.RUN_DONE:
65+
throw new Error('Statement has already been fully run');
3766
}
3867
let queryObject;
3968
if (this.isCLCommand) {
@@ -55,7 +84,7 @@ export class Query<T> {
5584
let result = await this.job.send(JSON.stringify(queryObject));
5685
let queryResult: QueryResult<T> = JSON.parse(result);
5786

58-
this.state = queryResult.is_done? QueryState.RUN_DONE: QueryState.RUN_MORE_DATA_AVAILABLE;
87+
this.state = queryResult.is_done ? QueryState.RUN_DONE : QueryState.RUN_MORE_DATA_AVAILABLE;
5988

6089
if (queryResult.success !== true && !this.isCLCommand) {
6190
this.state = QueryState.ERROR;
@@ -64,13 +93,14 @@ export class Query<T> {
6493
this.correlationId = queryResult.id;
6594
return queryResult;
6695
}
96+
6797
public async fetchMore(rowsToFetch: number = this.rowsToFetch): Promise<QueryResult<T>> {
6898
//TODO: verify that the SQL job hasn't changed
6999
switch (this.state) {
70-
case QueryState.NOT_YET_RUN:
71-
throw new Error('Statement has not yet been run');
72-
case QueryState.RUN_DONE:
73-
throw new Error('Statement has already been fully run');
100+
case QueryState.NOT_YET_RUN:
101+
throw new Error('Statement has not yet been run');
102+
case QueryState.RUN_DONE:
103+
throw new Error('Statement has already been fully run');
74104
}
75105
let queryObject = {
76106
id: SQLJob.getNewUniqueRequestId(`fetchMore`),
@@ -84,20 +114,32 @@ export class Query<T> {
84114
let result = await this.job.send(JSON.stringify(queryObject));
85115

86116
let queryResult: QueryResult<T> = JSON.parse(result);
87-
this.state = queryResult.is_done? QueryState.RUN_DONE: QueryState.RUN_MORE_DATA_AVAILABLE;
117+
this.state = queryResult.is_done ? QueryState.RUN_DONE : QueryState.RUN_MORE_DATA_AVAILABLE;
88118

89119
if (queryResult.success !== true) {
90120
this.state = QueryState.ERROR;
91121
throw new Error(queryResult.error || `Failed to run query (unknown error)`);
92122
}
93123
return queryResult;
94124
}
125+
95126
public async close() {
96-
this.state = QueryState.RUN_DONE;
127+
if (this.correlationId && this.state !== QueryState.RUN_DONE) {
128+
this.state = QueryState.RUN_DONE;
129+
let queryObject = {
130+
id: SQLJob.getNewUniqueRequestId(`sqlclose`),
131+
cont_id: this.correlationId,
132+
type: `sqlclose`,
133+
};
134+
135+
return this.job.send(JSON.stringify(queryObject));
136+
}
97137
}
138+
98139
public getId(): string {
99140
return this.correlationId;
100141
}
142+
101143
public getState(): QueryState {
102144
return this.state;
103145
}

src/connection/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ export enum ServerTraceDest {
3030
}
3131
export interface QueryOptions {
3232
isClCommand?: boolean,
33-
parameters?: any[]
33+
parameters?: any[],
34+
autoClose?: boolean
3435
}
3536
export interface SetConfigResult extends ServerResponse {
3637
tracedest: ServerTraceDest,

src/testing/jobs.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { JobStatus, SQLJob } from "../connection/sqlJob";
44
import { getInstance } from "../base";
55
import { ServerComponent } from "../connection/serverComponent";
66
import { ServerTraceDest, ServerTraceLevel } from "../connection/types";
7+
import { Query } from "../connection/query";
78

89
export const JobsSuite: TestSuite = {
910
name: `Connection tests`,
@@ -83,6 +84,35 @@ export const JobsSuite: TestSuite = {
8384
newJob.close();
8485
}},
8586

87+
{name: `Auto close statements`, test: async () => {
88+
let newJob = new SQLJob();
89+
await newJob.connect();
90+
91+
const autoCloseAnyway = newJob.query(`select * from QIWS.QCUSTCDT`, {autoClose: true});
92+
const noAutoClose = newJob.query(`select * from QIWS.QCUSTCDT`, {autoClose: false});
93+
const neverRuns = newJob.query(`select * from QIWS.QCUSTCDT`, {autoClose: true});
94+
95+
assert.strictEqual(Query.getOpenIds(newJob.id).length, 3);
96+
97+
// If we ran this, two both autoClose statements would be cleaned up
98+
// await Query.cleanup();
99+
100+
await Promise.all([autoCloseAnyway.run(1), noAutoClose.run(1)]);
101+
102+
assert.strictEqual(Query.getOpenIds(newJob.id).length, 3);
103+
104+
// Now cleanup should auto close autoCloseAnyway and neverRuns,
105+
// but not noAutoClose because it hasn't finished running
106+
await Query.cleanup();
107+
108+
const leftOverIds = Query.getOpenIds(newJob.id);
109+
assert.strictEqual(leftOverIds.length, 1);
110+
111+
assert.strictEqual(noAutoClose.getId(), leftOverIds[0]);
112+
113+
newJob.close();
114+
}},
115+
86116
{name: `CL Command (success)`, test: async () => {
87117
assert.strictEqual(ServerComponent.isInstalled(), true);
88118

@@ -103,6 +133,7 @@ export const JobsSuite: TestSuite = {
103133
assert.equal(CPF2880, true);
104134
newJob.close();
105135
}},
136+
106137
{name: `CL Command (error)`, test: async () => {
107138
assert.strictEqual(ServerComponent.isInstalled(), true);
108139

src/views/results/index.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,18 @@ class ResultSetPanelProvider {
3333
webviewView.webview.html = html.getLoadingHTML();
3434
this._view.webview.onDidReceiveMessage(async (message) => {
3535
if (message.query) {
36-
const instance = await getInstance();
37-
const content = instance.getContent();
38-
const config = instance.getConfig();
3936
let data = [];
4037

4138
let queryObject = Query.byId(message.queryId);
4239
try {
43-
if (undefined === queryObject) {
44-
let query = await JobManager.getPagingStatement(message.query, { isClCommand: message.isCL });
40+
if (queryObject === undefined) {
41+
// We will need to revisit this if we ever allow multiple result tabs like ACS does
42+
Query.cleanup();
43+
44+
let query = await JobManager.getPagingStatement(message.query, { isClCommand: message.isCL, autoClose: true });
4545
queryObject = query;
4646
}
47+
4748
let queryResults = queryObject.getState() == QueryState.RUN_MORE_DATA_AVAILABLE ? await queryObject.fetchMore() : await queryObject.run();
4849
data = queryResults.data;
4950
this._view.webview.postMessage({

0 commit comments

Comments
 (0)