-
Notifications
You must be signed in to change notification settings - Fork 34
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Iterable interface for IOperation (#252)
* Iterable interface for IOperation Signed-off-by: Levko Kravets <[email protected]> * Chore: split `utils` unit tests into few files Signed-off-by: Levko Kravets <[email protected]> * Add tests Signed-off-by: Levko Kravets <[email protected]> * Add visibility modifiers Signed-off-by: Levko Kravets <[email protected]> * Fixes after merge Signed-off-by: Levko Kravets <[email protected]> * Fix import Signed-off-by: Levko Kravets <[email protected]> --------- Signed-off-by: Levko Kravets <[email protected]>
- Loading branch information
1 parent
c239fca
commit fb817b5
Showing
7 changed files
with
484 additions
and
93 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
import IOperation, { IOperationChunksIterator, IOperationRowsIterator, IteratorOptions } from '../contracts/IOperation'; | ||
|
||
abstract class OperationIterator<R> implements AsyncIterableIterator<R> { | ||
public readonly operation: IOperation; | ||
|
||
protected readonly options?: IteratorOptions; | ||
|
||
constructor(operation: IOperation, options?: IteratorOptions) { | ||
this.operation = operation; | ||
this.options = options; | ||
} | ||
|
||
protected abstract getNext(): Promise<IteratorResult<R>>; | ||
|
||
public [Symbol.asyncIterator]() { | ||
return this; | ||
} | ||
|
||
public async next() { | ||
const result = await this.getNext(); | ||
|
||
if (result.done && this.options?.autoClose) { | ||
await this.operation.close(); | ||
} | ||
|
||
return result; | ||
} | ||
|
||
// This method is intended for a cleanup when the caller does not intend to make any more | ||
// reads from iterator (e.g. when using `break` in a `for ... of` loop) | ||
public async return(value?: any) { | ||
if (this.options?.autoClose) { | ||
await this.operation.close(); | ||
} | ||
|
||
return { done: true, value }; | ||
} | ||
} | ||
|
||
export class OperationChunksIterator extends OperationIterator<Array<object>> implements IOperationChunksIterator { | ||
protected async getNext(): Promise<IteratorResult<Array<object>>> { | ||
const hasMoreRows = await this.operation.hasMoreRows(); | ||
if (hasMoreRows) { | ||
const value = await this.operation.fetchChunk(this.options); | ||
return { done: false, value }; | ||
} | ||
|
||
return { done: true, value: undefined }; | ||
} | ||
} | ||
|
||
export class OperationRowsIterator extends OperationIterator<object> implements IOperationRowsIterator { | ||
private chunk: Array<object> = []; | ||
|
||
private index: number = 0; | ||
|
||
constructor(operation: IOperation, options?: IteratorOptions) { | ||
super(operation, { | ||
...options, | ||
// Tell slicer to return raw chunks. We're going to process rows one by one anyway, | ||
// so no need to additionally buffer and slice chunks returned by server | ||
disableBuffering: true, | ||
}); | ||
} | ||
|
||
protected async getNext(): Promise<IteratorResult<object>> { | ||
if (this.index < this.chunk.length) { | ||
const value = this.chunk[this.index]; | ||
this.index += 1; | ||
return { done: false, value }; | ||
} | ||
|
||
const hasMoreRows = await this.operation.hasMoreRows(); | ||
if (hasMoreRows) { | ||
this.chunk = await this.operation.fetchChunk(this.options); | ||
this.index = 0; | ||
// Note: this call is not really a recursion. Since this method is | ||
// async - the call will be actually scheduled for processing on | ||
// the next event loop cycle | ||
return this.getNext(); | ||
} | ||
|
||
return { done: true, value: undefined }; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
const { expect } = require('chai'); | ||
const sinon = require('sinon'); | ||
const config = require('./utils/config'); | ||
const { DBSQLClient } = require('../../lib'); | ||
|
||
async function openSession(customConfig) { | ||
const client = new DBSQLClient(); | ||
|
||
const clientConfig = client.getConfig(); | ||
sinon.stub(client, 'getConfig').returns({ | ||
...clientConfig, | ||
...customConfig, | ||
}); | ||
|
||
const connection = await client.connect({ | ||
host: config.host, | ||
path: config.path, | ||
token: config.token, | ||
}); | ||
|
||
return connection.openSession({ | ||
initialCatalog: config.database[0], | ||
initialSchema: config.database[1], | ||
}); | ||
} | ||
|
||
function arrayChunks(arr, chunkSize) { | ||
const result = []; | ||
|
||
while (arr.length > 0) { | ||
const chunk = arr.splice(0, chunkSize); | ||
result.push(chunk); | ||
} | ||
|
||
return result; | ||
} | ||
|
||
describe('Iterators', () => { | ||
it('should iterate over all chunks', async () => { | ||
const session = await openSession({ arrowEnabled: false }); | ||
sinon.spy(session.context.driver, 'fetchResults'); | ||
try { | ||
const expectedRowsCount = 10; | ||
|
||
// set `maxRows` to null to disable direct results so all the data are fetched through `driver.fetchResults` | ||
const operation = await session.executeStatement(`SELECT * FROM range(0, ${expectedRowsCount})`, { | ||
maxRows: null, | ||
}); | ||
|
||
const expectedRows = Array.from({ length: expectedRowsCount }, (_, id) => ({ id })); | ||
const chunkSize = 4; | ||
const expectedChunks = arrayChunks(expectedRows, chunkSize); | ||
|
||
let index = 0; | ||
for await (const chunk of operation.iterateChunks({ maxRows: chunkSize })) { | ||
expect(chunk).to.deep.equal(expectedChunks[index]); | ||
index += 1; | ||
} | ||
|
||
expect(index).to.equal(expectedChunks.length); | ||
} finally { | ||
await session.close(); | ||
} | ||
}); | ||
|
||
it('should iterate over all rows', async () => { | ||
const session = await openSession({ arrowEnabled: false }); | ||
sinon.spy(session.context.driver, 'fetchResults'); | ||
try { | ||
const expectedRowsCount = 10; | ||
|
||
const operation = await session.executeStatement(`SELECT * FROM range(0, ${expectedRowsCount})`); | ||
|
||
const expectedRows = Array.from({ length: expectedRowsCount }, (_, id) => ({ id })); | ||
|
||
let index = 0; | ||
for await (const row of operation.iterateRows()) { | ||
expect(row).to.deep.equal(expectedRows[index]); | ||
index += 1; | ||
} | ||
|
||
expect(index).to.equal(expectedRows.length); | ||
} finally { | ||
await session.close(); | ||
} | ||
}); | ||
}); |
94 changes: 1 addition & 93 deletions
94
tests/unit/utils.test.js → tests/unit/utils/CloseableCollection.test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.