Repository navigation
Expand file tree
/
Copy pathpause.test.js
More file actions
executable file
·517 lines (470 loc) · 16.6 KB
/
Copy pathpause.test.js
File metadata and controls
executable file
·517 lines (470 loc) · 16.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
'use strict'
const sql = require('../lib/sql')
const assert = require('chai').assert
const { TestEnv } = require('./env/test-env')
/* globals describe it beforeEach afterEach */
const { configureTestLogging } = require('./common/logging-helper')
// Configure logging based on environment variables
// By default, tests run silently. To enable logging:
// - MSNODESQLV8_TEST_VERBOSE=true npm test (for full trace logging)
// - MSNODESQLV8_TEST_LOG_LEVEL=DEBUG MSNODESQLV8_TEST_LOG_CONSOLE=true npm test
// - MSNODESQLV8_TEST_LOG_LEVEL=INFO MSNODESQLV8_TEST_LOG_FILE=/tmp/test.log npm test
configureTestLogging(sql)
describe('pause', function () {
// Test configuration
const TIMEOUT = 30000
const LARGE_QUERY_ROWS = 3000
const PAUSE_INTERVAL = 100
const RESUME_DELAY = 50
const PAUSE_CHECK_DELAY = 200
// Common test queries
const QUERIES = {
large: `select top ${LARGE_QUERY_ROWS} * from syscolumns`,
simple: 'select \'hello\'',
all: 'select * from syscolumns'
}
// Test environment
const env = new TestEnv()
this.timeout(TIMEOUT)
this.beforeEach(done => {
sql.logger.info('Starting test setup', 'params.test.beforeEach')
env.open().then(() => {
sql.logger.info('Test environment opened successfully', 'params.test.beforeEach')
done()
}).catch(e => {
sql.logger.error(`Failed to open test environment: ${e}`, 'params.test.beforeEach')
sql.logger.error(e)
})
})
this.afterEach(done => {
sql.logger.info('Starting test cleanup', 'params.test.afterEach')
env.close().then(() => {
sql.logger.info('Test environment closed successfully', 'params.test.afterEach')
done()
}).catch(e => {
sql.logger.error(`Failed to close test environment: ${e}`, 'params.test.afterEach')
sql.logger.error(e)
})
})
// Helper function to count rows from a query
async function countQueryRows (connection, query) {
return new Promise((resolve, reject) => {
let count = 0
const q = connection.query(query)
q.on('row', () => count++)
q.on('done', () => { resolve(count) })
q.on('error', reject)
})
}
// Helper function to create a query with row counting
function createRowCountingQuery (connection, query, options = {}) {
const q = connection.query(query)
let rows = 0
const result = {
query: q,
getRowCount: () => rows,
onRow: options.onRow || (() => {}),
// a query a test leaves paused is cancelled when the connection closes (HY008)
onError: options.onError || ((e) => { if (e?.sqlstate !== 'HY008') assert.ifError(e) }),
onDone: options.onDone || (() => {})
}
q.on('row', () => {
rows++
result.onRow(rows)
})
q.on('error', result.onError)
q.on('done', () => result.onDone(rows))
return result
}
describe('basic pause/resume functionality', () => {
it('should pause a large query and cancel without resume', function (done) {
// a paused query ends on cancel as any other: no more rows, an HY008 error, then free
let cancelErr
const queryHelper = createRowCountingQuery(env.theConnection, QUERIES.large, {
onRow: (rowCount) => {
assert(!cancelErr, 'no rows after a cancel')
if (rowCount % PAUSE_INTERVAL === 0) {
queryHelper.query.pauseQuery()
setTimeout(() => {
queryHelper.query.cancelQuery(e => { assert.ifError(e) })
}, RESUME_DELAY)
}
},
onError: e => { cancelErr = e }
})
queryHelper.query.on('free', () => {
assert(cancelErr, 'expected an error on cancel')
assert.strictEqual(cancelErr.sqlstate, 'HY008')
done()
})
})
it('should handle resume on a query that was never paused', async function () {
const expectedRows = await countQueryRows(env.theConnection, QUERIES.large)
return new Promise((resolve, reject) => {
const queryHelper = createRowCountingQuery(env.theConnection, QUERIES.large, {
onRow: () => {
queryHelper.query.resumeQuery() // Resume without pause
},
onDone: (rowCount) => {
try {
assert.strictEqual(rowCount, expectedRows)
resolve()
} catch (e) {
reject(e)
}
},
onError: reject
})
})
})
it('should allow queries to start in paused state', function (done) {
const q = env.theConnection.query(QUERIES.large)
q.pauseQuery() // Pause before any rows
let rowCount = 0
q.on('row', () => rowCount++)
q.on('error', (e) => { assert.ifError(e) })
setTimeout(() => {
assert.strictEqual(rowCount, 0, 'Should not receive any rows when starting paused')
q.on('free', () => {
assert(rowCount > 0, 'rows once resumed')
done()
})
q.resumeQuery()
}, PAUSE_CHECK_DELAY)
})
it('should pause and resume every N rows', async function () {
const expectedRows = await countQueryRows(env.theConnection, QUERIES.large)
return new Promise((resolve, reject) => {
const queryHelper = createRowCountingQuery(env.theConnection, QUERIES.large, {
onRow: (rowCount) => {
if (rowCount % PAUSE_INTERVAL === 0) {
queryHelper.query.pauseQuery()
setTimeout(() => {
queryHelper.query.resumeQuery()
}, RESUME_DELAY)
}
},
onDone: (rowCount) => {
try {
assert.strictEqual(rowCount, expectedRows)
resolve()
} catch (e) {
reject(e)
}
},
onError: reject
})
})
})
})
describe('edge cases', () => {
it('should handle pause on a completed query', function (done) {
const queryHelper = createRowCountingQuery(env.theConnection, QUERIES.simple, {
onDone: (rowCount) => {
assert.strictEqual(rowCount, 1)
queryHelper.query.pauseQuery() // Should not throw
done()
}
})
})
it('should handle multiple pause calls', function (done) {
const queryHelper = createRowCountingQuery(env.theConnection, QUERIES.simple, {
onRow: () => {
queryHelper.query.pauseQuery()
queryHelper.query.pauseQuery() // Double pause
// nothing more, not even done, until resumed - and one resume is enough
setTimeout(() => queryHelper.query.resumeQuery(), PAUSE_CHECK_DELAY)
},
onDone: (rowCount) => {
assert.strictEqual(rowCount, 1)
done()
}
})
})
})
describe('query interaction', () => {
it('should refuse a new query while paused - HY010, done, free', function (done) {
const pauseAt = 10
const queryHelper = createRowCountingQuery(env.theConnection, QUERIES.large, {
onRow: (rowCount) => {
if (rowCount !== pauseAt) return
queryHelper.query.pauseQuery()
const events = []
const q2 = env.theConnection.query(QUERIES.large, (err, res) => {
events.push('cb')
assert(err, 'refused while the first query is paused')
assert.strictEqual(err.sqlstate, 'HY010')
assert.strictEqual(res, null)
})
q2.on('submitted', () => events.push('submitted'))
q2.on('done', () => events.push('done'))
q2.on('free', () => {
events.push('free')
assert.deepStrictEqual(events, ['cb', 'done', 'free'])
assert.strictEqual(queryHelper.getRowCount(), pauseAt)
// the paused query is untouched: resume it and it completes
queryHelper.query.resumeQuery()
})
},
onDone: rowCount => {
assert(rowCount > pauseAt)
done()
}
})
})
it('should run a query queued before the pause once the paused one completes', function (done) {
const pauseAt = 10
let resumed = false
let firstFree = false
const queryHelper = createRowCountingQuery(env.theConnection, QUERIES.large, {
onRow: (rowCount) => {
if (rowCount === pauseAt) {
queryHelper.query.pauseQuery()
setTimeout(() => {
resumed = true
queryHelper.query.resumeQuery()
}, PAUSE_CHECK_DELAY)
}
}
})
queryHelper.query.on('free', () => { firstFree = true })
// queued while the first query runs, before it is paused: waits its turn
const q2 = env.theConnection.query(QUERIES.large, (err, res) => {
assert.ifError(err)
assert(firstFree, 'the paused query finishes first')
assert(res.length > 0)
done()
})
q2.on('submitted', () => assert(resumed, 'not submitted while the first is paused'))
})
it('should refuse a transaction while a query is paused', function (done) {
const q = env.theConnection.query(QUERIES.large)
q.pauseQuery()
setTimeout(() => {
env.theConnection.beginTransaction(err => {
assert(err)
assert.strictEqual(err.sqlstate, 'HY010')
q.on('done', () => done())
q.resumeQuery()
})
}, PAUSE_CHECK_DELAY)
})
it('should allow new query after cancel', function (done) {
const queryHelper = createRowCountingQuery(env.theConnection, QUERIES.large, {
onRow: (rowCount) => {
if (rowCount % PAUSE_INTERVAL === 0) {
queryHelper.query.pauseQuery()
setTimeout(() => {
queryHelper.query.cancelQuery((err) => {
assert.ifError(err)
// Submit new query after cancel
env.theConnection.query(QUERIES.large, (err, res) => {
assert.ifError(err)
assert(res.length > 0)
done()
})
})
}, RESUME_DELAY)
}
}
})
})
it('should handle pause and resume with subsequent query', async function () {
const expectedRows = await countQueryRows(env.theConnection, QUERIES.large)
return new Promise((resolve, reject) => {
const queryHelper = createRowCountingQuery(env.theConnection, QUERIES.large, {
onRow: (rowCount) => {
if (rowCount % PAUSE_INTERVAL === 0) {
queryHelper.query.pauseQuery()
setTimeout(() => {
queryHelper.query.resumeQuery()
}, RESUME_DELAY)
}
},
onDone: (rowCount) => {
assert.strictEqual(rowCount, expectedRows)
// Run another query after completion
env.theConnection.query(QUERIES.large, (err, res) => {
try {
assert.ifError(err)
assert.strictEqual(res.length, expectedRows)
resolve()
} catch (e) {
reject(e)
}
})
},
onError: reject
})
})
})
})
describe('connection handling', () => {
it('should handle connection close with paused query', function (done) {
env.sql.open(env.connectionString, (err, newConn) => {
assert.ifError(err)
const q = newConn.query(QUERIES.large)
q.pauseQuery()
let rowCount = 0
let cancelErr
let freed = false
q.on('row', () => rowCount++)
q.on('error', (e) => { cancelErr = e })
q.on('free', () => { freed = true })
// Wait a bit to ensure no rows are received, then close: the close cancels the
// paused query, which ends and frees its statement before the connection closes
setTimeout(() => {
assert.strictEqual(rowCount, 0, 'Should not receive rows while paused')
newConn.close((err) => {
assert.ifError(err)
assert.strictEqual(cancelErr?.sqlstate, 'HY008')
assert(freed, 'the paused query is freed before the close completes')
done()
})
}, PAUSE_CHECK_DELAY * 5) // Longer wait for this test
})
})
it('should stop at specified row count when paused', function (done) {
const pauseAt = 10
const queryHelper = createRowCountingQuery(env.theConnection, QUERIES.large, {
onRow: (rowCount) => {
if (rowCount === pauseAt) {
queryHelper.query.pauseQuery()
setTimeout(() => {
assert.strictEqual(queryHelper.getRowCount(), pauseAt)
done()
}, PAUSE_CHECK_DELAY)
}
}
})
})
})
describe('end-of-resultset pause/resume (issue #389)', () => {
// These tests cover the scenario where pause is called near the end of a
// result set. When the native ODBC batch is the final batch (endOfRows=true)
// and the query is paused mid-batch, resume must not call nativeGetRows again
// or the ODBC driver will crash (Windows: 0xC0000005) or return a function
// sequence error (Linux).
it('should handle pause near end of small result set', async function () {
const totalRows = 50
const query = `select top ${totalRows} * from syscolumns`
const pauseAt = totalRows - 5
return new Promise((resolve, reject) => {
let rows = 0
const q = env.theConnection.query(query)
q.on('error', reject)
q.on('row', () => {
rows++
if (rows === pauseAt) {
q.pauseQuery()
setTimeout(() => {
q.resumeQuery()
}, RESUME_DELAY)
}
})
q.on('done', () => {
try {
assert.strictEqual(rows, totalRows)
resolve()
} catch (e) {
reject(e)
}
})
})
})
it('should handle repeated pause/resume cycles across page boundaries', async function () {
const totalRows = 100
const query = `select top ${totalRows} * from syscolumns`
const pageSize = 10
return new Promise((resolve, reject) => {
let rows = 0
const q = env.theConnection.query(query)
q.on('error', reject)
q.on('row', () => {
rows++
if (rows % pageSize === 0) {
q.pauseQuery()
setTimeout(() => {
q.resumeQuery()
}, RESUME_DELAY)
}
})
q.on('done', () => {
try {
assert.strictEqual(rows, totalRows)
resolve()
} catch (e) {
reject(e)
}
})
})
})
it('should complete normally after pause near end then run subsequent query', async function () {
const totalRows = 50
const query = `select top ${totalRows} * from syscolumns`
const pauseAt = totalRows - 5
// First: run with pause near end
await new Promise((resolve, reject) => {
let rows = 0
const q = env.theConnection.query(query)
q.on('error', reject)
q.on('row', () => {
rows++
if (rows === pauseAt) {
q.pauseQuery()
setTimeout(() => {
q.resumeQuery()
}, RESUME_DELAY)
}
})
q.on('done', () => {
try {
assert.strictEqual(rows, totalRows)
resolve()
} catch (e) {
reject(e)
}
})
})
// Second: run a normal query to verify connection is still usable
const secondRows = await countQueryRows(env.theConnection, query)
assert.strictEqual(secondRows, totalRows)
})
it('should handle multiple pause/resume near end in sequence', async function () {
const totalRows = 50
const query = `select top ${totalRows} * from syscolumns`
for (let iteration = 0; iteration < 5; iteration++) {
await new Promise((resolve, reject) => {
let rows = 0
const q = env.theConnection.query(query)
q.on('error', reject)
q.on('row', () => {
rows++
if (rows === totalRows - 3) {
q.pauseQuery()
setTimeout(() => {
q.resumeQuery()
}, RESUME_DELAY)
}
})
q.on('done', () => {
try {
assert.strictEqual(rows, totalRows)
resolve()
} catch (e) {
reject(e)
}
})
})
}
})
})
describe('performance', () => {
it('should handle large query without pause', function (done) {
const q = env.theConnection.query(QUERIES.all)
q.on('error', (e) => { assert.ifError(e) })
q.on('row', () => {}) // Process rows
q.on('done', done)
})
})
})