dbtour-source-mssql 0.7.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +10 -2
- package/dist/index.js +77 -2
- package/dist/map.d.ts +25 -1
- package/dist/map.js +57 -0
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import type
|
|
2
|
-
export { mapRecordsets, toRecordsets, formatType } from './map.ts';
|
|
1
|
+
import { type Database, type QueryResult, type RunOptions, type SourceConfig } from 'dbtour';
|
|
2
|
+
export { mapRecordsets, RowCollector, sizeOf, toRecordsets, formatType } from './map.ts';
|
|
3
3
|
export declare const query: string;
|
|
4
4
|
export declare function extract(config: SourceConfig): Promise<Database>;
|
|
5
|
+
/**
|
|
6
|
+
* Runs one query dbtour has checked, with `connection` already the source's reader, a read-only login. It runs
|
|
7
|
+
* inside a transaction that is always rolled back, so nothing it did is kept even if the login could write. Its
|
|
8
|
+
* rows stream into a collector that stops at the limit or the byte budget, whichever comes first, and the query
|
|
9
|
+
* is cancelled there, so neither the rows nor their size is ever more than a row past what is kept; SET ROWCOUNT
|
|
10
|
+
* stops the server one row past the limit too. A timer cancels it at the timeout.
|
|
11
|
+
*/
|
|
12
|
+
export declare function run(config: SourceConfig, text: string, options: RunOptions): Promise<QueryResult>;
|
package/dist/index.js
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
// mssql:// URL. It is read from the environment variable dbtour.yaml names, never from the file.
|
|
4
4
|
import { readFileSync } from 'node:fs';
|
|
5
5
|
import sql from 'mssql';
|
|
6
|
-
import {
|
|
7
|
-
|
|
6
|
+
import { maxResultBytes } from 'dbtour';
|
|
7
|
+
import { mapRecordsets, RowCollector, toRecordsets } from "./map.js";
|
|
8
|
+
export { mapRecordsets, RowCollector, sizeOf, toRecordsets, formatType } from "./map.js";
|
|
8
9
|
export const query = readFileSync(new URL('./query.sql', import.meta.url), 'utf8');
|
|
9
10
|
export async function extract(config) {
|
|
10
11
|
if (!config.connection) {
|
|
@@ -21,3 +22,77 @@ export async function extract(config) {
|
|
|
21
22
|
await pool.close();
|
|
22
23
|
}
|
|
23
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Some seconds past dbtour's own timeout for the driver's: the driver's default (15 seconds) would otherwise end
|
|
27
|
+
* a query first, with a message that says nothing of what to do, so dbtour's cancel always comes first.
|
|
28
|
+
*/
|
|
29
|
+
const driverGraceMs = 5000;
|
|
30
|
+
/**
|
|
31
|
+
* Runs one query dbtour has checked, with `connection` already the source's reader, a read-only login. It runs
|
|
32
|
+
* inside a transaction that is always rolled back, so nothing it did is kept even if the login could write. Its
|
|
33
|
+
* rows stream into a collector that stops at the limit or the byte budget, whichever comes first, and the query
|
|
34
|
+
* is cancelled there, so neither the rows nor their size is ever more than a row past what is kept; SET ROWCOUNT
|
|
35
|
+
* stops the server one row past the limit too. A timer cancels it at the timeout.
|
|
36
|
+
*/
|
|
37
|
+
export async function run(config, text, options) {
|
|
38
|
+
if (!config.connection)
|
|
39
|
+
throw new Error(`Source ${config.name} has no reader connection: add "reader: \${DBTOUR_MSSQL_READER}" to it in dbtour.yaml and set that variable`);
|
|
40
|
+
const pool = new sql.ConnectionPool(config.connection);
|
|
41
|
+
await pool.connect();
|
|
42
|
+
// The driver takes a per-transaction timeout, cascaded to its requests; its type definitions predate that.
|
|
43
|
+
const begin = pool.transaction;
|
|
44
|
+
const transaction = begin.call(pool, { requestTimeout: options.timeoutMs + driverGraceMs });
|
|
45
|
+
const rows = new RowCollector(options.limit, maxResultBytes);
|
|
46
|
+
let timedOut = false;
|
|
47
|
+
try {
|
|
48
|
+
await transaction.begin();
|
|
49
|
+
const request = transaction.request();
|
|
50
|
+
// Rows as arrays, so two columns with the same name both come back; streamed, so they are counted as they come.
|
|
51
|
+
request.arrayRowMode = true;
|
|
52
|
+
request.stream = true;
|
|
53
|
+
let cancelling = false;
|
|
54
|
+
const cancel = () => {
|
|
55
|
+
if (!cancelling) {
|
|
56
|
+
cancelling = true;
|
|
57
|
+
request.cancel();
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const timer = setTimeout(() => {
|
|
61
|
+
timedOut = true;
|
|
62
|
+
cancel();
|
|
63
|
+
}, options.timeoutMs);
|
|
64
|
+
try {
|
|
65
|
+
await new Promise((done, fail) => {
|
|
66
|
+
request.on('recordset', (columns) => rows.start(columns));
|
|
67
|
+
request.on('row', (row) => {
|
|
68
|
+
if (!rows.add(row))
|
|
69
|
+
cancel();
|
|
70
|
+
});
|
|
71
|
+
// A cancel ends in an error the driver raises; it is only a failure when dbtour did not ask for it. The
|
|
72
|
+
// error comes before the request has finished, and a rollback asked for then is refused and the connection
|
|
73
|
+
// stays held, so pool.close() would wait for ever: the first error is kept, and given once 'done' follows.
|
|
74
|
+
let failure;
|
|
75
|
+
request.on('error', (error) => {
|
|
76
|
+
if (!cancelling)
|
|
77
|
+
failure ??= error;
|
|
78
|
+
});
|
|
79
|
+
request.on('done', () => (failure ? fail(failure) : done()));
|
|
80
|
+
// The semicolon before the query lets it start with WITH, which T-SQL wants after a statement.
|
|
81
|
+
void request.query(`SET ROWCOUNT ${options.limit + 1};\n${text};\nSET ROWCOUNT 0;`);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
clearTimeout(timer);
|
|
86
|
+
}
|
|
87
|
+
if (timedOut) {
|
|
88
|
+
const seconds = options.timeoutMs / 1000;
|
|
89
|
+
throw new Error(`The query ran past ${seconds} ${seconds === 1 ? 'second' : 'seconds'} and was cancelled: narrow it with a filter on an indexed column, or TOP`);
|
|
90
|
+
}
|
|
91
|
+
return rows.result();
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
// Always rolled back: a read keeps nothing, and a write that got past every other guard is undone.
|
|
95
|
+
await transaction.rollback().catch(() => undefined);
|
|
96
|
+
await pool.close();
|
|
97
|
+
}
|
|
98
|
+
}
|
package/dist/map.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Database } from 'dbtour';
|
|
1
|
+
import type { Database, QueryResult } from 'dbtour';
|
|
2
2
|
/** node-mssql returns a bigint as a string, since it may not fit a JavaScript number. */
|
|
3
3
|
type Count = number | string;
|
|
4
4
|
/** Whether the sensitivity labels were read: the view is absent before SQL Server 2019, or the login may not. */
|
|
@@ -177,4 +177,28 @@ export declare function toRecordsets(sets: unknown[][]): Recordsets;
|
|
|
177
177
|
export declare function mapRecordsets(sets: Recordsets, options?: MapOptions): Database;
|
|
178
178
|
/** The type as SQL Server would spell it in a CREATE TABLE: nvarchar(320), varchar(max), decimal(18,2), datetime2(7). */
|
|
179
179
|
export declare function formatType(row: Pick<ColumnRow, 'type' | 'maxLength' | 'precision' | 'scale'>): string;
|
|
180
|
+
/** Roughly how many bytes a value holds in memory: text two to a character, bytes as they are. */
|
|
181
|
+
export declare function sizeOf(value: unknown): number;
|
|
182
|
+
/**
|
|
183
|
+
* The rows of a query as they stream in, kept until one is past the limit or they outgrow the byte budget.
|
|
184
|
+
* add() says whether to go on; run() cancels the query when it says no, so neither the rows nor their size
|
|
185
|
+
* is ever more than one row past what is kept.
|
|
186
|
+
*/
|
|
187
|
+
export declare class RowCollector {
|
|
188
|
+
private columns;
|
|
189
|
+
private rows;
|
|
190
|
+
private bytes;
|
|
191
|
+
private stopped?;
|
|
192
|
+
private readonly limit;
|
|
193
|
+
private readonly budget;
|
|
194
|
+
constructor(limit: number, budget: number);
|
|
195
|
+
/** A new result set: its columns, and its rows from nothing. */
|
|
196
|
+
start(columns: {
|
|
197
|
+
name: string;
|
|
198
|
+
}[]): void;
|
|
199
|
+
add(row: unknown[]): boolean;
|
|
200
|
+
/** Whether add() has stopped it; a cancelled query's error is then expected, not a failure. */
|
|
201
|
+
get done(): boolean;
|
|
202
|
+
result(): QueryResult;
|
|
203
|
+
}
|
|
180
204
|
export {};
|
package/dist/map.js
CHANGED
|
@@ -286,3 +286,60 @@ function groupBy(rows, key) {
|
|
|
286
286
|
function pattern(text) {
|
|
287
287
|
return new RegExp('^' + text.split('*').map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('.*') + '$', 'i');
|
|
288
288
|
}
|
|
289
|
+
/** Roughly how many bytes a value holds in memory: text two to a character, bytes as they are. */
|
|
290
|
+
export function sizeOf(value) {
|
|
291
|
+
if (value === null || value === undefined)
|
|
292
|
+
return 1;
|
|
293
|
+
if (typeof value === 'string')
|
|
294
|
+
return value.length * 2;
|
|
295
|
+
if (value instanceof Uint8Array)
|
|
296
|
+
return value.length;
|
|
297
|
+
return 8;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* The rows of a query as they stream in, kept until one is past the limit or they outgrow the byte budget.
|
|
301
|
+
* add() says whether to go on; run() cancels the query when it says no, so neither the rows nor their size
|
|
302
|
+
* is ever more than one row past what is kept.
|
|
303
|
+
*/
|
|
304
|
+
export class RowCollector {
|
|
305
|
+
columns = [];
|
|
306
|
+
rows = [];
|
|
307
|
+
bytes = 0;
|
|
308
|
+
stopped;
|
|
309
|
+
limit;
|
|
310
|
+
budget;
|
|
311
|
+
constructor(limit, budget) {
|
|
312
|
+
this.limit = limit;
|
|
313
|
+
this.budget = budget;
|
|
314
|
+
}
|
|
315
|
+
/** A new result set: its columns, and its rows from nothing. */
|
|
316
|
+
start(columns) {
|
|
317
|
+
this.columns = columns.map((c, i) => c.name || `(column ${i + 1})`);
|
|
318
|
+
this.rows = [];
|
|
319
|
+
this.bytes = 0;
|
|
320
|
+
}
|
|
321
|
+
add(row) {
|
|
322
|
+
if (this.rows.length >= this.limit) {
|
|
323
|
+
this.stopped = 'limit';
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
const size = row.reduce((sum, value) => sum + sizeOf(value), 0);
|
|
327
|
+
if (this.bytes + size > this.budget) {
|
|
328
|
+
this.stopped = 'size';
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
this.rows.push(row);
|
|
332
|
+
this.bytes += size;
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
/** Whether add() has stopped it; a cancelled query's error is then expected, not a failure. */
|
|
336
|
+
get done() {
|
|
337
|
+
return this.stopped !== undefined;
|
|
338
|
+
}
|
|
339
|
+
result() {
|
|
340
|
+
const result = { columns: this.columns, rows: this.rows, truncated: this.stopped !== undefined, limit: this.limit };
|
|
341
|
+
if (this.stopped)
|
|
342
|
+
result.reason = this.stopped;
|
|
343
|
+
return result;
|
|
344
|
+
}
|
|
345
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dbtour-source-mssql",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "SQL Server source for dbtour: tables, views, columns, keys, relations, indexes, descriptions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -36,6 +36,6 @@
|
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/mssql": "^9.1.8",
|
|
39
|
-
"dbtour": "0.
|
|
39
|
+
"dbtour": "0.9.0"
|
|
40
40
|
}
|
|
41
41
|
}
|