@eltjs/sdk-microsoft-sql-server 0.1.3 → 0.3.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/README.md +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/sql-server-database.js +26 -18
- package/dist/sql-server-request.d.ts +10 -2
- package/dist/sql-server-request.js +59 -8
- package/dist/sql-server-session.d.ts +5 -4
- package/dist/sql-server-session.js +21 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,6 +18,7 @@ for (const table of await session.tables()) {
|
|
|
18
18
|
- `SqlServerDatabase` takes an ADO.NET connection string. The string must name its server and its database. An `mssql://` URL does not work.
|
|
19
19
|
- `open` gives a `SqlServerSession`. Dispose the session to close its connections.
|
|
20
20
|
- `tables` gives each user table that the login can read. A view is not in the list. Each `SqlServerTable` has its columns, its primary key and its Change Tracking state. `unreadable` names the columns that the login cannot read.
|
|
21
|
+
- `rows`, `page`, `rowversions` and `changes` take a `SqlServerRead`: a `clock`, which an app gives as `globalThis`, and a `signal`, which stops the read also when the server sends nothing. A read that gets no row between two checks an hour apart fails with `SqlServerUnavailableError`.
|
|
21
22
|
- `rows`, `page` and `rowversions` read rows of a table. `changes` gives the rows that changed after a `ChangePosition`, each row once.
|
|
22
23
|
- `version`, `statistics`, `rowCounts` and `lastWrites` give the values that show that a table changed.
|
|
23
24
|
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export { type ChangePosition, type SqlServerChange } from './change-set.ts';
|
|
|
2
2
|
export { ChangeHistoryExpiredError, SqlServerLoginError, SqlServerPermissionError, SqlServerUnavailableError, SqlServerVersionError, } from './errors.ts';
|
|
3
3
|
export { SqlServerColumn } from './sql-server-column.ts';
|
|
4
4
|
export { SqlServerDatabase } from './sql-server-database.ts';
|
|
5
|
+
export { type SqlServerRead } from './sql-server-request.ts';
|
|
5
6
|
export { SqlServerRow } from './sql-server-row.ts';
|
|
6
7
|
export { SqlServerSession } from './sql-server-session.ts';
|
|
7
8
|
export { SqlServerTable } from './sql-server-table.ts';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import sql from 'mssql';
|
|
2
2
|
import { cellNumber } from './cells.js';
|
|
3
|
-
import {
|
|
4
|
-
import { query } from './sql-server-request.js';
|
|
3
|
+
import { SqlServerVersionError } from './errors.js';
|
|
4
|
+
import { connectionFailure, query } from './sql-server-request.js';
|
|
5
5
|
import { SqlServerSession } from './sql-server-session.js';
|
|
6
6
|
// One database on a SQL Server, named by an ADO.NET connection string
|
|
7
7
|
// (Server=host,port;Database=name;User Id=...;Password=...), the form SQL
|
|
@@ -23,7 +23,12 @@ export class SqlServerDatabase {
|
|
|
23
23
|
this.location = `${config.server}${instance ? `\\${instance}` : ''}:${config.port ?? 1433}/${config.database}`;
|
|
24
24
|
this.name = config.database;
|
|
25
25
|
// A read streams a whole table, so no time limit fits every table.
|
|
26
|
-
this.#config = {
|
|
26
|
+
this.#config = {
|
|
27
|
+
...config,
|
|
28
|
+
requestTimeout: 0,
|
|
29
|
+
pool: { min: 0, max: 4 },
|
|
30
|
+
beforeConnect: markBusyLogin,
|
|
31
|
+
};
|
|
27
32
|
Object.freeze(this);
|
|
28
33
|
}
|
|
29
34
|
async open() {
|
|
@@ -32,12 +37,10 @@ export class SqlServerDatabase {
|
|
|
32
37
|
await pool.connect();
|
|
33
38
|
}
|
|
34
39
|
catch (cause) {
|
|
35
|
-
throw
|
|
36
|
-
? new SqlServerLoginError(this.location, cause)
|
|
37
|
-
: new SqlServerUnavailableError(this.location, cause);
|
|
40
|
+
throw connectionFailure(this.location, cause);
|
|
38
41
|
}
|
|
39
42
|
try {
|
|
40
|
-
const [[edition, major, snapshot] = []] = await query(pool.request(), `SELECT CAST(SERVERPROPERTY('EngineEdition') AS int), CAST(SERVERPROPERTY('ProductMajorVersion') AS int), CAST(snapshot_isolation_state AS int) FROM sys.databases WHERE database_id = DB_ID()`);
|
|
43
|
+
const [[edition, major, snapshot] = []] = await query(pool.request(), this.location, `SELECT CAST(SERVERPROPERTY('EngineEdition') AS int), CAST(SERVERPROPERTY('ProductMajorVersion') AS int), CAST(snapshot_isolation_state AS int) FROM sys.databases WHERE database_id = DB_ID()`);
|
|
41
44
|
const version = cellNumber(major, 'ProductMajorVersion');
|
|
42
45
|
// Azure SQL Database (5) and Managed Instance (8) report version 12
|
|
43
46
|
// but run the current engine.
|
|
@@ -51,15 +54,20 @@ export class SqlServerDatabase {
|
|
|
51
54
|
}
|
|
52
55
|
}
|
|
53
56
|
}
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
57
|
+
// The login errors SQL Server raises while it fails over or is busy.
|
|
58
|
+
const busyLoginErrors = new Set([4060, 10928, 10929, 40197, 40501, 40613]);
|
|
59
|
+
// Marks a failed login as transient when the server raised one of those errors
|
|
60
|
+
// on the way. tedious keeps only the last error of a login, and SQL Server
|
|
61
|
+
// follows 4060 with 18456, which alone means a refused login. tedious also
|
|
62
|
+
// leaves its own mark off the error of its last retry, so a database that stays
|
|
63
|
+
// unavailable through its retries would read as refused.
|
|
64
|
+
function markBusyLogin(connection) {
|
|
65
|
+
let busy = false;
|
|
66
|
+
connection.on('errorMessage', ({ number }) => {
|
|
67
|
+
busy ||= busyLoginErrors.has(number);
|
|
68
|
+
});
|
|
69
|
+
connection.once('connect', (error) => {
|
|
70
|
+
if (error !== undefined && busy)
|
|
71
|
+
error.isTransient = true;
|
|
72
|
+
});
|
|
65
73
|
}
|
|
@@ -1,3 +1,11 @@
|
|
|
1
1
|
import sql from 'mssql';
|
|
2
|
-
export
|
|
3
|
-
|
|
2
|
+
export type SqlServerRead = {
|
|
3
|
+
readonly clock: {
|
|
4
|
+
setTimeout(callback: () => void, ms: number): unknown;
|
|
5
|
+
clearTimeout(timer: unknown): void;
|
|
6
|
+
};
|
|
7
|
+
readonly signal: AbortSignal | undefined;
|
|
8
|
+
};
|
|
9
|
+
export declare function connectionFailure(location: string, cause: unknown): Error;
|
|
10
|
+
export declare function query(request: sql.Request, location: string, text: string, parameters?: readonly string[]): Promise<unknown[][]>;
|
|
11
|
+
export declare function stream(request: sql.Request, text: string, parameters: readonly string[], { clock, signal }: SqlServerRead, location: string): AsyncGenerator<unknown[]>;
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import sql from 'mssql';
|
|
2
|
-
import { SqlServerPermissionError } from './errors.js';
|
|
2
|
+
import { SqlServerLoginError, SqlServerPermissionError, SqlServerUnavailableError, } from './errors.js';
|
|
3
|
+
// How often a read checks that rows still arrive. A read that got no row from
|
|
4
|
+
// one check to the next fails, so a server or network that stopped
|
|
5
|
+
// sending fails it within two hours, the limit of Airbyte's source-mssql
|
|
6
|
+
// (maxSecondsBetweenMessages: 7200 in its metadata.yaml). The driver has no such
|
|
7
|
+
// limit: its request timeout stops at the first data, and a read of a whole
|
|
8
|
+
// table can take any length of time.
|
|
9
|
+
const silenceCheckMs = 3_600_000;
|
|
3
10
|
// Every parameter travels as text and the statement casts it, so a value
|
|
4
11
|
// keeps the exact spelling a read gave it, whatever the driver would make of
|
|
5
12
|
// a number or a Date.
|
|
@@ -7,8 +14,35 @@ function bind(request, parameters) {
|
|
|
7
14
|
for (const [index, value] of parameters.entries())
|
|
8
15
|
request.input(`p${index}`, sql.NVarChar(sql.MAX), value);
|
|
9
16
|
}
|
|
10
|
-
//
|
|
11
|
-
|
|
17
|
+
// What a failed connection means, when the pool opened it or a statement lost
|
|
18
|
+
// it: a login the server refused is the user's to fix, and anything else can
|
|
19
|
+
// succeed at a later attempt.
|
|
20
|
+
export function connectionFailure(location, cause) {
|
|
21
|
+
return refused(cause)
|
|
22
|
+
? new SqlServerLoginError(location, cause)
|
|
23
|
+
: new SqlServerUnavailableError(location, cause);
|
|
24
|
+
}
|
|
25
|
+
// Whether the server answered and refused the login. Its errors while it fails
|
|
26
|
+
// over or is busy are marked as transient when the connection opens
|
|
27
|
+
// (SqlServerDatabase); those mean the server cannot be reached now.
|
|
28
|
+
function refused(error) {
|
|
29
|
+
if (!(error instanceof sql.ConnectionError) || error.code !== 'ELOGIN')
|
|
30
|
+
return false;
|
|
31
|
+
const { originalError } = error;
|
|
32
|
+
return !(originalError !== undefined &&
|
|
33
|
+
'isTransient' in originalError &&
|
|
34
|
+
originalError.isTransient === true);
|
|
35
|
+
}
|
|
36
|
+
// A failed statement as what it means. The connection failed under it when
|
|
37
|
+
// the error has no server number, such as ECONNRESET, or when the server ended
|
|
38
|
+
// the session: SQL Server ends the connection at severity 20 and above, as
|
|
39
|
+
// error 596 does for a session that was killed. A refused read names the grant
|
|
40
|
+
// it needs (errors 229 and 230).
|
|
41
|
+
function readError(error, location) {
|
|
42
|
+
if (error instanceof sql.ConnectionError ||
|
|
43
|
+
(error instanceof sql.RequestError &&
|
|
44
|
+
(typeof error.number !== 'number' || Number(error.class) >= 20)))
|
|
45
|
+
return connectionFailure(location, error);
|
|
12
46
|
if (!(error instanceof sql.RequestError))
|
|
13
47
|
return error;
|
|
14
48
|
if (error.number !== 229 && error.number !== 230)
|
|
@@ -26,7 +60,7 @@ function asRow(row) {
|
|
|
26
60
|
return row;
|
|
27
61
|
}
|
|
28
62
|
// Every row of one statement, each as the values it selects in order.
|
|
29
|
-
export async function query(request, text, parameters = []) {
|
|
63
|
+
export async function query(request, location, text, parameters = []) {
|
|
30
64
|
request.arrayRowMode = true;
|
|
31
65
|
bind(request, parameters);
|
|
32
66
|
try {
|
|
@@ -34,12 +68,14 @@ export async function query(request, text, parameters = []) {
|
|
|
34
68
|
return recordset.map(asRow);
|
|
35
69
|
}
|
|
36
70
|
catch (error) {
|
|
37
|
-
throw readError(error);
|
|
71
|
+
throw readError(error, location);
|
|
38
72
|
}
|
|
39
73
|
}
|
|
40
74
|
// The rows of one statement as the driver receives them; stopping early
|
|
41
|
-
// cancels the statement so its connection returns to the pool.
|
|
42
|
-
|
|
75
|
+
// cancels the statement so its connection returns to the pool. The signal stops
|
|
76
|
+
// the read also while the driver has no row to give.
|
|
77
|
+
export async function* stream(request, text, parameters, { clock, signal }, location) {
|
|
78
|
+
signal?.throwIfAborted();
|
|
43
79
|
request.arrayRowMode = true;
|
|
44
80
|
bind(request, parameters);
|
|
45
81
|
const rows = request.toReadableStream();
|
|
@@ -49,17 +85,32 @@ export async function* stream(request, text, parameters = []) {
|
|
|
49
85
|
void ended.then(() => {
|
|
50
86
|
finished = true;
|
|
51
87
|
});
|
|
88
|
+
let arrived = false;
|
|
89
|
+
const check = () => {
|
|
90
|
+
if (arrived) {
|
|
91
|
+
arrived = false;
|
|
92
|
+
timer = clock.setTimeout(check, silenceCheckMs);
|
|
93
|
+
}
|
|
94
|
+
else
|
|
95
|
+
rows.destroy(new SqlServerUnavailableError(location, new Error(`The server sent no row for over ${silenceCheckMs} ms`)));
|
|
96
|
+
};
|
|
97
|
+
let timer = clock.setTimeout(check, silenceCheckMs);
|
|
98
|
+
const stop = () => rows.destroy(signal?.reason);
|
|
99
|
+
signal?.addEventListener('abort', stop, { once: true });
|
|
52
100
|
// In stream mode the promise always resolves; failures arrive on rows.
|
|
53
101
|
void request.query(text);
|
|
54
102
|
try {
|
|
55
103
|
for await (const row of rows) {
|
|
104
|
+
arrived = true;
|
|
56
105
|
yield asRow(row);
|
|
57
106
|
}
|
|
58
107
|
}
|
|
59
108
|
catch (error) {
|
|
60
|
-
throw readError(error);
|
|
109
|
+
throw readError(error, location);
|
|
61
110
|
}
|
|
62
111
|
finally {
|
|
112
|
+
clock.clearTimeout(timer);
|
|
113
|
+
signal?.removeEventListener('abort', stop);
|
|
63
114
|
// A reader that stops early cancels the statement, and waits for it to
|
|
64
115
|
// end: until then the connection, or a transaction on it, is not free.
|
|
65
116
|
if (!finished) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import sql from 'mssql';
|
|
2
2
|
import { type ChangePosition, ChangeSet } from './change-set.ts';
|
|
3
|
+
import { type SqlServerRead } from './sql-server-request.ts';
|
|
3
4
|
import { SqlServerRow } from './sql-server-row.ts';
|
|
4
5
|
import { SqlServerTable } from './sql-server-table.ts';
|
|
5
6
|
export type SqlServerVersion = {
|
|
@@ -17,8 +18,8 @@ export declare class SqlServerSession implements AsyncDisposable {
|
|
|
17
18
|
lastWrites(): Promise<ReadonlyMap<string, string>>;
|
|
18
19
|
changeTrackingPosition(table: SqlServerTable): Promise<ChangePosition>;
|
|
19
20
|
minActiveRowversion(): Promise<string>;
|
|
20
|
-
page(table: SqlServerTable, after: readonly string[] | null, limit: number): AsyncGenerator<SqlServerRow>;
|
|
21
|
-
rowversions(table: SqlServerTable, above: string, below: string, limit: number): AsyncGenerator<SqlServerRow>;
|
|
22
|
-
rows(table: SqlServerTable): AsyncGenerator<SqlServerRow>;
|
|
23
|
-
changes(table: SqlServerTable, since: ChangePosition): Promise<ChangeSet>;
|
|
21
|
+
page(table: SqlServerTable, after: readonly string[] | null, limit: number, read: SqlServerRead): AsyncGenerator<SqlServerRow>;
|
|
22
|
+
rowversions(table: SqlServerTable, above: string, below: string, limit: number, read: SqlServerRead): AsyncGenerator<SqlServerRow>;
|
|
23
|
+
rows(table: SqlServerTable, read: SqlServerRead): AsyncGenerator<SqlServerRow>;
|
|
24
|
+
changes(table: SqlServerTable, since: ChangePosition, read: SqlServerRead): Promise<ChangeSet>;
|
|
24
25
|
}
|
|
@@ -104,9 +104,9 @@ export class SqlServerSession {
|
|
|
104
104
|
// login can read.
|
|
105
105
|
async tables() {
|
|
106
106
|
const [tables, columns, keys] = await Promise.all([
|
|
107
|
-
query(this.#pool.request(), tablesQuery),
|
|
108
|
-
query(this.#pool.request(), columnsQuery),
|
|
109
|
-
query(this.#pool.request(), primaryKeysQuery),
|
|
107
|
+
query(this.#pool.request(), this.#location, tablesQuery),
|
|
108
|
+
query(this.#pool.request(), this.#location, columnsQuery),
|
|
109
|
+
query(this.#pool.request(), this.#location, primaryKeysQuery),
|
|
110
110
|
]);
|
|
111
111
|
const columnsOf = groupBy(columns);
|
|
112
112
|
const keysOf = groupBy(keys);
|
|
@@ -152,7 +152,7 @@ export class SqlServerSession {
|
|
|
152
152
|
});
|
|
153
153
|
}
|
|
154
154
|
async version() {
|
|
155
|
-
const [[changeTracking, rowversion] = []] = await query(this.#pool.request(), 'SELECT CONVERT(varchar(20), CHANGE_TRACKING_CURRENT_VERSION()), CONVERT(varchar(20), CAST(@@DBTS AS bigint))');
|
|
155
|
+
const [[changeTracking, rowversion] = []] = await query(this.#pool.request(), this.#location, 'SELECT CONVERT(varchar(20), CHANGE_TRACKING_CURRENT_VERSION()), CONVERT(varchar(20), CAST(@@DBTS AS bigint))');
|
|
156
156
|
return Object.freeze({
|
|
157
157
|
changeTracking: cellNullableText(changeTracking, 'change tracking version'),
|
|
158
158
|
rowversion: cellText(rowversion, '@@DBTS'),
|
|
@@ -165,7 +165,7 @@ export class SqlServerSession {
|
|
|
165
165
|
// statistics has no count, so its value never moves. A login that can read
|
|
166
166
|
// a table can read its counts.
|
|
167
167
|
async statistics() {
|
|
168
|
-
const rows = await query(this.#pool.request(), statisticsQuery);
|
|
168
|
+
const rows = await query(this.#pool.request(), this.#location, statisticsQuery);
|
|
169
169
|
return new Map(rows.map(([table, counts]) => [
|
|
170
170
|
cellText(table, 'table'),
|
|
171
171
|
cellText(counts, 'statistics'),
|
|
@@ -176,7 +176,7 @@ export class SqlServerSession {
|
|
|
176
176
|
// also in a table whose statistics SQL Server has not built, but an update
|
|
177
177
|
// does not. A login that can read a table can read its count.
|
|
178
178
|
async rowCounts() {
|
|
179
|
-
const rows = await query(this.#pool.request(), rowCountsQuery);
|
|
179
|
+
const rows = await query(this.#pool.request(), this.#location, rowCountsQuery);
|
|
180
180
|
return new Map(rows.map(([table, count]) => [
|
|
181
181
|
cellText(table, 'table'),
|
|
182
182
|
cellText(count, 'row count'),
|
|
@@ -188,7 +188,7 @@ export class SqlServerSession {
|
|
|
188
188
|
// so the value of each table is then empty. A login needs VIEW SERVER
|
|
189
189
|
// STATE to read them (VIEW DATABASE STATE on Azure SQL Database).
|
|
190
190
|
async lastWrites() {
|
|
191
|
-
const rows = await query(this.#pool.request(), lastWritesQuery).catch((error) => {
|
|
191
|
+
const rows = await query(this.#pool.request(), this.#location, lastWritesQuery).catch((error) => {
|
|
192
192
|
// 297: the login may not read the state of the server or database.
|
|
193
193
|
if (error instanceof sql.RequestError && error.number === 297)
|
|
194
194
|
throw new SqlServerPermissionError('VIEW SERVER STATE (VIEW DATABASE STATE on Azure SQL Database)', error);
|
|
@@ -203,7 +203,7 @@ export class SqlServerSession {
|
|
|
203
203
|
// before the rows, so a change made during the read is read again, never
|
|
204
204
|
// missed.
|
|
205
205
|
async changeTrackingPosition(table) {
|
|
206
|
-
const [[version, generation] = []] = await query(this.#pool.request(), `SELECT CONVERT(varchar(20), CHANGE_TRACKING_CURRENT_VERSION()), ${generationQuery}`, [table.quoted]);
|
|
206
|
+
const [[version, generation] = []] = await query(this.#pool.request(), this.#location, `SELECT CONVERT(varchar(20), CHANGE_TRACKING_CURRENT_VERSION()), ${generationQuery}`, [table.quoted]);
|
|
207
207
|
if (version === null || generation === null)
|
|
208
208
|
throw new TypeError(`Change Tracking is off for ${table.quoted} in ${this.#location}`);
|
|
209
209
|
return Object.freeze({
|
|
@@ -214,13 +214,13 @@ export class SqlServerSession {
|
|
|
214
214
|
// Rows below this rowversion are committed; one at or above it may belong
|
|
215
215
|
// to a transaction still open, so a read stops short of it.
|
|
216
216
|
async minActiveRowversion() {
|
|
217
|
-
const [[value] = []] = await query(this.#pool.request(), 'SELECT CONVERT(varchar(20), CAST(MIN_ACTIVE_ROWVERSION() AS bigint))');
|
|
217
|
+
const [[value] = []] = await query(this.#pool.request(), this.#location, 'SELECT CONVERT(varchar(20), CAST(MIN_ACTIVE_ROWVERSION() AS bigint))');
|
|
218
218
|
return cellText(value, 'MIN_ACTIVE_ROWVERSION');
|
|
219
219
|
}
|
|
220
220
|
// Up to limit rows in primary key order, after the given position. The
|
|
221
221
|
// whole key compares as a tuple, each part cast to its column's type, so
|
|
222
222
|
// pages neither skip nor repeat rows that share a leading key value.
|
|
223
|
-
async *page(table, after, limit) {
|
|
223
|
+
async *page(table, after, limit, read) {
|
|
224
224
|
if (!table.pageable)
|
|
225
225
|
throw new TypeError(`${table.quoted} has no primary key a read can page by`);
|
|
226
226
|
const keys = table.primaryKey;
|
|
@@ -236,20 +236,20 @@ export class SqlServerSession {
|
|
|
236
236
|
`${key(index)} > ${cast(index)}`,
|
|
237
237
|
].join(' AND ')})`)
|
|
238
238
|
.join(' OR ')}`;
|
|
239
|
-
yield* this.#rows(table, `SELECT TOP (${count(limit)}) ${selection(table)} FROM ${table.quoted} AS t ${where} ORDER BY ${keys.map((_, index) => key(index)).join(', ')}`, after ?? []);
|
|
239
|
+
yield* this.#rows(table, `SELECT TOP (${count(limit)}) ${selection(table)} FROM ${table.quoted} AS t ${where} ORDER BY ${keys.map((_, index) => key(index)).join(', ')}`, after ?? [], read);
|
|
240
240
|
}
|
|
241
241
|
// Up to limit rows whose rowversion is above `above` and below `below`, in
|
|
242
242
|
// rowversion order.
|
|
243
|
-
async *rowversions(table, above, below, limit) {
|
|
243
|
+
async *rowversions(table, above, below, limit, read) {
|
|
244
244
|
const column = table.rowversion;
|
|
245
245
|
if (column === undefined)
|
|
246
246
|
throw new TypeError(`${table.quoted} has no rowversion column`);
|
|
247
247
|
const version = `t.${column.quoted}`;
|
|
248
|
-
yield* this.#rows(table, `SELECT TOP (${count(limit)}) ${selection(table)} FROM ${table.quoted} AS t WHERE ${version} > ${column.codec.cast('@p0')} AND ${version} < ${column.codec.cast('@p1')} ORDER BY ${version}`, [above, below]);
|
|
248
|
+
yield* this.#rows(table, `SELECT TOP (${count(limit)}) ${selection(table)} FROM ${table.quoted} AS t WHERE ${version} > ${column.codec.cast('@p0')} AND ${version} < ${column.codec.cast('@p1')} ORDER BY ${version}`, [above, below], read);
|
|
249
249
|
}
|
|
250
250
|
// Every row, in no particular order.
|
|
251
|
-
async *rows(table) {
|
|
252
|
-
yield* this.#rows(table, `SELECT ${selection(table)} FROM ${table.quoted} AS t`, []);
|
|
251
|
+
async *rows(table, read) {
|
|
252
|
+
yield* this.#rows(table, `SELECT ${selection(table)} FROM ${table.quoted} AS t`, [], read);
|
|
253
253
|
}
|
|
254
254
|
// What changed in the table since a position in its change history: each
|
|
255
255
|
// changed key once, with its row as it is now, or as a deletion. A position
|
|
@@ -259,7 +259,8 @@ export class SqlServerSession {
|
|
|
259
259
|
// first, so a change made during the read is read again next time, and the
|
|
260
260
|
// check runs again after the read in case cleanup removed history
|
|
261
261
|
// meanwhile.
|
|
262
|
-
async changes(table, since) {
|
|
262
|
+
async changes(table, since, read) {
|
|
263
|
+
const location = this.#location;
|
|
263
264
|
if (table.primaryKey.length === 0)
|
|
264
265
|
throw new TypeError(`${table.quoted} has no primary key to track changes by`);
|
|
265
266
|
const transaction = this.#snapshot
|
|
@@ -277,7 +278,7 @@ export class SqlServerSession {
|
|
|
277
278
|
? this.#pool.request()
|
|
278
279
|
: new sql.Request(transaction);
|
|
279
280
|
const check = async () => {
|
|
280
|
-
const [[current, minimum, viewable, generation, begin] = []] = await query(request(), `SELECT CONVERT(varchar(20), CHANGE_TRACKING_CURRENT_VERSION()), CONVERT(varchar(20), CHANGE_TRACKING_MIN_VALID_VERSION(OBJECT_ID(@p0))), HAS_PERMS_BY_NAME(@p0, 'OBJECT', 'VIEW CHANGE TRACKING'), ${generationQuery}, (SELECT CONVERT(varchar(20), begin_version) FROM sys.change_tracking_tables WHERE object_id = OBJECT_ID(@p0))`, [table.quoted]);
|
|
281
|
+
const [[current, minimum, viewable, generation, begin] = []] = await query(request(), location, `SELECT CONVERT(varchar(20), CHANGE_TRACKING_CURRENT_VERSION()), CONVERT(varchar(20), CHANGE_TRACKING_MIN_VALID_VERSION(OBJECT_ID(@p0))), HAS_PERMS_BY_NAME(@p0, 'OBJECT', 'VIEW CHANGE TRACKING'), ${generationQuery}, (SELECT CONVERT(varchar(20), begin_version) FROM sys.change_tracking_tables WHERE object_id = OBJECT_ID(@p0))`, [table.quoted]);
|
|
281
282
|
// Without the grant SQL Server still reports a minimum valid version
|
|
282
283
|
// (0), so the grant itself is checked first.
|
|
283
284
|
if (viewable !== 1)
|
|
@@ -306,7 +307,7 @@ SELECT ${present}, ${changed.join(', ')}, ${selection(table)}
|
|
|
306
307
|
FROM CHANGETABLE(CHANGES ${table.quoted}, @since) AS ct
|
|
307
308
|
LEFT JOIN ${table.quoted} AS t ON ${keys.map((column) => `t.${column.quoted} = ct.${column.quoted}`).join(' AND ')}`;
|
|
308
309
|
const changes = async function* () {
|
|
309
|
-
for await (const values of stream(request(), text, [since.version])) {
|
|
310
|
+
for await (const values of stream(request(), text, [since.version], read, location)) {
|
|
310
311
|
const [exists] = values;
|
|
311
312
|
const key = decode(keys, values.slice(1, 1 + changed.length));
|
|
312
313
|
yield exists === 1
|
|
@@ -331,8 +332,8 @@ LEFT JOIN ${table.quoted} AS t ON ${keys.map((column) => `t.${column.quoted} = c
|
|
|
331
332
|
throw error;
|
|
332
333
|
}
|
|
333
334
|
}
|
|
334
|
-
async *#rows(table, text, parameters) {
|
|
335
|
-
for await (const values of stream(this.#pool.request(), text, parameters))
|
|
335
|
+
async *#rows(table, text, parameters, read) {
|
|
336
|
+
for await (const values of stream(this.#pool.request(), text, parameters, read, this.#location))
|
|
336
337
|
yield new SqlServerRow(table, decode(table.columns, values));
|
|
337
338
|
}
|
|
338
339
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eltjs/sdk-microsoft-sql-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Reads one SQL Server database: discovery, exact reads, Change Tracking and change signals. It knows nothing of elt.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|