@eltjs/sdk-microsoft-sql-server 0.0.0-stage → 0.1.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.
Files changed (59) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +31 -2
  3. package/dist/cells.d.ts +4 -0
  4. package/dist/cells.js +19 -0
  5. package/dist/change-set.d.ts +20 -0
  6. package/dist/change-set.js +21 -0
  7. package/dist/codecs/boolean-codec.d.ts +10 -0
  8. package/dist/codecs/boolean-codec.js +20 -0
  9. package/dist/codecs/bytes-codec.d.ts +12 -0
  10. package/dist/codecs/bytes-codec.js +31 -0
  11. package/dist/codecs/codec-for.d.ts +12 -0
  12. package/dist/codecs/codec-for.js +106 -0
  13. package/dist/codecs/date-codec.d.ts +9 -0
  14. package/dist/codecs/date-codec.js +15 -0
  15. package/dist/codecs/decimal-codec.d.ts +13 -0
  16. package/dist/codecs/decimal-codec.js +27 -0
  17. package/dist/codecs/fraction.d.ts +1 -0
  18. package/dist/codecs/fraction.js +7 -0
  19. package/dist/codecs/int64-codec.d.ts +11 -0
  20. package/dist/codecs/int64-codec.js +26 -0
  21. package/dist/codecs/integer-codec.d.ts +11 -0
  22. package/dist/codecs/integer-codec.js +23 -0
  23. package/dist/codecs/local-time-codec.d.ts +12 -0
  24. package/dist/codecs/local-time-codec.js +21 -0
  25. package/dist/codecs/local-timestamp-codec.d.ts +12 -0
  26. package/dist/codecs/local-timestamp-codec.js +25 -0
  27. package/dist/codecs/number-codec.d.ts +11 -0
  28. package/dist/codecs/number-codec.js +24 -0
  29. package/dist/codecs/spatial-codec.d.ts +8 -0
  30. package/dist/codecs/spatial-codec.js +14 -0
  31. package/dist/codecs/sql-server-codec.d.ts +41 -0
  32. package/dist/codecs/sql-server-codec.js +21 -0
  33. package/dist/codecs/text-codec.d.ts +11 -0
  34. package/dist/codecs/text-codec.js +24 -0
  35. package/dist/codecs/timestamp-codec.d.ts +14 -0
  36. package/dist/codecs/timestamp-codec.js +35 -0
  37. package/dist/codecs/variant-codec.d.ts +9 -0
  38. package/dist/codecs/variant-codec.js +20 -0
  39. package/dist/errors.d.ts +21 -0
  40. package/dist/errors.js +44 -0
  41. package/dist/index.d.ts +8 -0
  42. package/dist/index.js +7 -0
  43. package/dist/quote-name.d.ts +1 -0
  44. package/dist/quote-name.js +4 -0
  45. package/dist/sql-server-column.d.ts +19 -0
  46. package/dist/sql-server-column.js +24 -0
  47. package/dist/sql-server-database.d.ts +8 -0
  48. package/dist/sql-server-database.js +65 -0
  49. package/dist/sql-server-request.d.ts +3 -0
  50. package/dist/sql-server-request.js +71 -0
  51. package/dist/sql-server-row.d.ts +10 -0
  52. package/dist/sql-server-row.js +25 -0
  53. package/dist/sql-server-session.d.ts +24 -0
  54. package/dist/sql-server-session.js +374 -0
  55. package/dist/sql-server-table.d.ts +23 -0
  56. package/dist/sql-server-table.js +36 -0
  57. package/dist/values.d.ts +11 -0
  58. package/dist/values.js +22 -0
  59. package/package.json +46 -4
@@ -0,0 +1,23 @@
1
+ import { SqlServerCodec } from './sql-server-codec.js';
2
+ // tinyint, smallint and int, which a JavaScript number holds exactly.
3
+ export class IntegerCodec extends SqlServerCodec {
4
+ value = { kind: 'integer' };
5
+ #type;
6
+ constructor(type) {
7
+ super();
8
+ this.#type = type;
9
+ }
10
+ select(reference) {
11
+ return [reference];
12
+ }
13
+ decode([value]) {
14
+ if (value === null)
15
+ return null;
16
+ if (!Number.isSafeInteger(value))
17
+ throw new TypeError(`SQL Server returned ${String(value)} for ${this.#type}`);
18
+ return Number(value);
19
+ }
20
+ cast(parameter) {
21
+ return `CAST(${parameter} AS ${this.#type})`;
22
+ }
23
+ }
@@ -0,0 +1,12 @@
1
+ import { SqlServerCodec } from './sql-server-codec.ts';
2
+ export declare class LocalTimeCodec extends SqlServerCodec<string> {
3
+ #private;
4
+ readonly value: {
5
+ readonly kind: 'local-time';
6
+ readonly precision: number;
7
+ };
8
+ constructor(declared: string, precision: number);
9
+ select(reference: string): readonly string[];
10
+ decode([value]: readonly unknown[]): string | null;
11
+ cast(parameter: string): string;
12
+ }
@@ -0,0 +1,21 @@
1
+ import { SqlServerCodec, text } from './sql-server-codec.js';
2
+ // time: a time of day, which the default style spells with exactly its
3
+ // precision's fraction digits, zeros included.
4
+ export class LocalTimeCodec extends SqlServerCodec {
5
+ value;
6
+ #declared;
7
+ constructor(declared, precision) {
8
+ super();
9
+ this.value = { kind: 'local-time', precision };
10
+ this.#declared = declared;
11
+ }
12
+ select(reference) {
13
+ return [`CONVERT(varchar(16), ${reference})`];
14
+ }
15
+ decode([value]) {
16
+ return value === null ? null : text(value, this.#declared);
17
+ }
18
+ cast(parameter) {
19
+ return `CAST(${parameter} AS ${this.#declared})`;
20
+ }
21
+ }
@@ -0,0 +1,12 @@
1
+ import { SqlServerCodec } from './sql-server-codec.ts';
2
+ export declare class LocalTimestampCodec extends SqlServerCodec<string> {
3
+ #private;
4
+ readonly value: {
5
+ readonly kind: 'local-timestamp';
6
+ readonly precision: number;
7
+ };
8
+ constructor(declared: string, precision: number);
9
+ select(reference: string): readonly string[];
10
+ decode([value]: readonly unknown[]): string | null;
11
+ cast(parameter: string): string;
12
+ }
@@ -0,0 +1,25 @@
1
+ import { withFraction } from './fraction.js';
2
+ import { SqlServerCodec, text } from './sql-server-codec.js';
3
+ // datetime2, datetime and smalldatetime: a wall-clock time with no zone, to
4
+ // its precision's fraction digits, which the driver's millisecond Date would
5
+ // round.
6
+ export class LocalTimestampCodec extends SqlServerCodec {
7
+ value;
8
+ #declared;
9
+ constructor(declared, precision) {
10
+ super();
11
+ this.value = { kind: 'local-timestamp', precision };
12
+ this.#declared = declared;
13
+ }
14
+ select(reference) {
15
+ return [`CONVERT(varchar(27), ${reference}, 126)`];
16
+ }
17
+ decode([value]) {
18
+ return value === null
19
+ ? null
20
+ : withFraction(text(value, this.#declared), this.value.precision);
21
+ }
22
+ cast(parameter) {
23
+ return `CONVERT(${this.#declared}, ${parameter}, 126)`;
24
+ }
25
+ }
@@ -0,0 +1,11 @@
1
+ import { SqlServerCodec } from './sql-server-codec.ts';
2
+ export declare class NumberCodec extends SqlServerCodec<number> {
3
+ #private;
4
+ readonly value: {
5
+ readonly kind: 'number';
6
+ };
7
+ constructor(type: string);
8
+ select(reference: string): readonly string[];
9
+ decode([value]: readonly unknown[]): number | null;
10
+ cast(parameter: string): string;
11
+ }
@@ -0,0 +1,24 @@
1
+ import { SqlServerCodec } from './sql-server-codec.js';
2
+ // real and float, IEEE values a JavaScript number holds exactly; its shortest
3
+ // spelling reads back as the same value.
4
+ export class NumberCodec extends SqlServerCodec {
5
+ value = { kind: 'number' };
6
+ #type;
7
+ constructor(type) {
8
+ super();
9
+ this.#type = type;
10
+ }
11
+ select(reference) {
12
+ return [reference];
13
+ }
14
+ decode([value]) {
15
+ if (value === null)
16
+ return null;
17
+ if (typeof value !== 'number')
18
+ throw new TypeError(`SQL Server returned a ${typeof value} for ${this.#type}`);
19
+ return value;
20
+ }
21
+ cast(parameter) {
22
+ return `CAST(${parameter} AS ${this.#type})`;
23
+ }
24
+ }
@@ -0,0 +1,8 @@
1
+ import { SqlServerCodec } from './sql-server-codec.ts';
2
+ export declare class SpatialCodec extends SqlServerCodec<string> {
3
+ readonly value: {
4
+ readonly kind: 'spatial';
5
+ };
6
+ select(reference: string): readonly string[];
7
+ decode([value]: readonly unknown[]): string | null;
8
+ }
@@ -0,0 +1,14 @@
1
+ import { SqlServerCodec, text } from './sql-server-codec.js';
2
+ // geography and geometry as extended well-known text: the SRID, then every
3
+ // coordinate with its Z and M, which plain well-known text drops.
4
+ export class SpatialCodec extends SqlServerCodec {
5
+ value = { kind: 'spatial' };
6
+ select(reference) {
7
+ return [
8
+ `'SRID=' + CONVERT(varchar(11), ${reference}.STSrid) + ';' + ${reference}.AsTextZM()`,
9
+ ];
10
+ }
11
+ decode([value]) {
12
+ return value === null ? null : text(value, 'spatial');
13
+ }
14
+ }
@@ -0,0 +1,41 @@
1
+ import type { SqlServerValue } from '../values.ts';
2
+ export type ValueKind = {
3
+ readonly kind: 'text';
4
+ } | {
5
+ readonly kind: 'integer';
6
+ } | {
7
+ readonly kind: 'int64';
8
+ } | {
9
+ readonly kind: 'boolean';
10
+ } | {
11
+ readonly kind: 'number';
12
+ } | {
13
+ readonly kind: 'decimal';
14
+ readonly precision: number;
15
+ readonly scale: number;
16
+ } | {
17
+ readonly kind: 'date';
18
+ } | {
19
+ readonly kind: 'local-timestamp';
20
+ readonly precision: number;
21
+ } | {
22
+ readonly kind: 'timestamp';
23
+ readonly precision: number;
24
+ } | {
25
+ readonly kind: 'local-time';
26
+ readonly precision: number;
27
+ } | {
28
+ readonly kind: 'bytes';
29
+ } | {
30
+ readonly kind: 'spatial';
31
+ } | {
32
+ readonly kind: 'variant';
33
+ };
34
+ export declare abstract class SqlServerCodec<Value extends SqlServerValue> {
35
+ abstract readonly value: ValueKind;
36
+ abstract select(reference: string): readonly string[];
37
+ abstract decode(values: readonly unknown[]): Value | null;
38
+ cast(_parameter: string): string | undefined;
39
+ position(value: Value): string;
40
+ }
41
+ export declare function text(value: unknown, type: string): string;
@@ -0,0 +1,21 @@
1
+ // How a read takes one SQL Server type exactly. The driver hands decimal and
2
+ // money over as JavaScript numbers and datetime2 as a millisecond Date, and
3
+ // drops a datetimeoffset's offset, so each codec selects text SQL Server
4
+ // spells exactly, and decodes it into the value's canonical form.
5
+ export class SqlServerCodec {
6
+ // A key column pages through a table: its value as text, and the SQL that
7
+ // turns a parameter holding that text back into the column's type, so a
8
+ // comparison orders as the column sorts. A type that cannot be compared,
9
+ // such as xml, returns undefined.
10
+ cast(_parameter) {
11
+ return undefined;
12
+ }
13
+ position(value) {
14
+ return String(value);
15
+ }
16
+ }
17
+ export function text(value, type) {
18
+ if (typeof value !== 'string')
19
+ throw new TypeError(`SQL Server returned a ${typeof value} for ${type}`);
20
+ return value;
21
+ }
@@ -0,0 +1,11 @@
1
+ import { SqlServerCodec } from './sql-server-codec.ts';
2
+ export declare class TextCodec extends SqlServerCodec<string> {
3
+ #private;
4
+ readonly value: {
5
+ readonly kind: 'text';
6
+ };
7
+ constructor(type: string, read: (reference: string) => string, cast?: (parameter: string) => string);
8
+ select(reference: string): readonly string[];
9
+ decode([value]: readonly unknown[]): string | null;
10
+ cast(parameter: string): string | undefined;
11
+ }
@@ -0,0 +1,24 @@
1
+ import { SqlServerCodec, text } from './sql-server-codec.js';
2
+ // Character types, and the types SQL Server spells as text: uniqueidentifier,
3
+ // xml, hierarchyid and json.
4
+ export class TextCodec extends SqlServerCodec {
5
+ value = { kind: 'text' };
6
+ #type;
7
+ #read;
8
+ #cast;
9
+ constructor(type, read, cast) {
10
+ super();
11
+ this.#type = type;
12
+ this.#read = read;
13
+ this.#cast = cast;
14
+ }
15
+ select(reference) {
16
+ return [this.#read(reference)];
17
+ }
18
+ decode([value]) {
19
+ return value === null ? null : text(value, this.#type);
20
+ }
21
+ cast(parameter) {
22
+ return this.#cast?.(parameter);
23
+ }
24
+ }
@@ -0,0 +1,14 @@
1
+ import { OffsetTimestamp } from '../values.ts';
2
+ import { SqlServerCodec } from './sql-server-codec.ts';
3
+ export declare class TimestampCodec extends SqlServerCodec<OffsetTimestamp> {
4
+ #private;
5
+ readonly value: {
6
+ readonly kind: 'timestamp';
7
+ readonly precision: number;
8
+ };
9
+ constructor(declared: string, precision: number);
10
+ select(reference: string): readonly string[];
11
+ decode([instant, offset]: readonly unknown[]): OffsetTimestamp | null;
12
+ cast(parameter: string): string;
13
+ position(value: OffsetTimestamp): string;
14
+ }
@@ -0,0 +1,35 @@
1
+ import { OffsetTimestamp } from '../values.js';
2
+ import { withFraction } from './fraction.js';
3
+ import { SqlServerCodec, text } from './sql-server-codec.js';
4
+ // datetimeoffset: its UTC instant to the column's precision, and the offset
5
+ // it was written with, in minutes. The driver keeps neither the seventh
6
+ // digit nor the offset.
7
+ export class TimestampCodec extends SqlServerCodec {
8
+ value;
9
+ #declared;
10
+ constructor(declared, precision) {
11
+ super();
12
+ this.value = { kind: 'timestamp', precision };
13
+ this.#declared = declared;
14
+ }
15
+ select(reference) {
16
+ return [
17
+ `CONVERT(varchar(27), CAST(SWITCHOFFSET(${reference}, 0) AS datetime2(${this.value.precision})), 126)`,
18
+ `DATEPART(TZOFFSET, ${reference})`,
19
+ ];
20
+ }
21
+ decode([instant, offset]) {
22
+ if (instant === null)
23
+ return null;
24
+ if (!Number.isSafeInteger(offset))
25
+ throw new TypeError(`SQL Server returned offset ${String(offset)}`);
26
+ return new OffsetTimestamp(`${withFraction(text(instant, this.#declared), this.value.precision)}Z`, Number(offset));
27
+ }
28
+ // datetimeoffset values compare by their instant, whatever their offset.
29
+ cast(parameter) {
30
+ return `CONVERT(${this.#declared}, ${parameter}, 127)`;
31
+ }
32
+ position(value) {
33
+ return value.instant;
34
+ }
35
+ }
@@ -0,0 +1,9 @@
1
+ import { VariantValue } from '../values.ts';
2
+ import { SqlServerCodec } from './sql-server-codec.ts';
3
+ export declare class VariantCodec extends SqlServerCodec<VariantValue> {
4
+ readonly value: {
5
+ readonly kind: 'variant';
6
+ };
7
+ select(reference: string): readonly string[];
8
+ decode([value, type]: readonly unknown[]): VariantValue | null;
9
+ }
@@ -0,0 +1,20 @@
1
+ import { VariantValue } from '../values.js';
2
+ import { SqlServerCodec, text } from './sql-server-codec.js';
3
+ // sql_variant: the value as text, by the style that keeps its base type
4
+ // exact (126 for times, 3 for floating point, 1 for bytes in hex), and the
5
+ // base type itself.
6
+ export class VariantCodec extends SqlServerCodec {
7
+ value = { kind: 'variant' };
8
+ select(reference) {
9
+ const type = `SQL_VARIANT_PROPERTY(${reference}, 'BaseType')`;
10
+ return [
11
+ `CASE WHEN ${type} IN ('binary', 'varbinary') THEN CONVERT(nvarchar(max), CAST(${reference} AS varbinary(8000)), 1) WHEN ${type} IN ('float', 'real') THEN CONVERT(nvarchar(max), CAST(${reference} AS float), 3) ELSE CONVERT(nvarchar(max), ${reference}, 126) END`,
12
+ `CONVERT(varchar(128), ${type})`,
13
+ ];
14
+ }
15
+ decode([value, type]) {
16
+ return value === null
17
+ ? null
18
+ : new VariantValue(text(value, 'sql_variant'), text(type, 'sql_variant'));
19
+ }
20
+ }
@@ -0,0 +1,21 @@
1
+ export declare class SqlServerUnavailableError extends Error {
2
+ name: string;
3
+ constructor(location: string, cause: unknown);
4
+ }
5
+ export declare class SqlServerLoginError extends Error {
6
+ name: string;
7
+ constructor(location: string, cause: unknown);
8
+ }
9
+ export declare class SqlServerPermissionError extends Error {
10
+ name: string;
11
+ readonly grant: string;
12
+ constructor(grant: string, cause?: unknown);
13
+ }
14
+ export declare class SqlServerVersionError extends Error {
15
+ name: string;
16
+ constructor(location: string, version: number);
17
+ }
18
+ export declare class ChangeHistoryExpiredError extends Error {
19
+ name: string;
20
+ constructor(table: string, since: string);
21
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,44 @@
1
+ // The server could not be reached: it is down, restarting or failing over,
2
+ // its address is wrong, or the network dropped the connection. A later
3
+ // attempt can succeed.
4
+ export class SqlServerUnavailableError extends Error {
5
+ name = 'SqlServerUnavailableError';
6
+ constructor(location, cause) {
7
+ super(`SQL Server ${location} cannot be reached.`, { cause });
8
+ }
9
+ }
10
+ // The server answered and refused the login: the credentials are wrong, the
11
+ // login is disabled, or it cannot open the database. Only the user can fix
12
+ // it.
13
+ export class SqlServerLoginError extends Error {
14
+ name = 'SqlServerLoginError';
15
+ constructor(location, cause) {
16
+ super(`SQL Server ${location} refused the login.`, { cause });
17
+ }
18
+ }
19
+ // The login lacks a permission a read needs. grant names what to give it.
20
+ export class SqlServerPermissionError extends Error {
21
+ name = 'SqlServerPermissionError';
22
+ grant;
23
+ constructor(grant, cause) {
24
+ super(`SQL Server refused the read; grant the login ${grant}.`, { cause });
25
+ this.grant = grant;
26
+ }
27
+ }
28
+ // Reading needs SQL Server 2016 or later (or Azure SQL): the masking and
29
+ // external-table metadata and the text styles the reads rely on start there.
30
+ export class SqlServerVersionError extends Error {
31
+ name = 'SqlServerVersionError';
32
+ constructor(location, version) {
33
+ super(`SQL Server ${location} is version ${version}; reading it needs SQL Server 2016 (13) or later.`);
34
+ }
35
+ }
36
+ // The saved Change Tracking version is no longer one the table's change
37
+ // history covers: cleanup removed what followed it, the table was truncated,
38
+ // or the database was restored. The table has to be read in full again.
39
+ export class ChangeHistoryExpiredError extends Error {
40
+ name = 'ChangeHistoryExpiredError';
41
+ constructor(table, since) {
42
+ super(`The change history of ${table} no longer covers version ${since}; read the table in full again.`);
43
+ }
44
+ }
@@ -0,0 +1,8 @@
1
+ export { type ChangePosition, type SqlServerChange } from './change-set.ts';
2
+ export { ChangeHistoryExpiredError, SqlServerLoginError, SqlServerPermissionError, SqlServerUnavailableError, SqlServerVersionError, } from './errors.ts';
3
+ export { SqlServerColumn } from './sql-server-column.ts';
4
+ export { SqlServerDatabase } from './sql-server-database.ts';
5
+ export { SqlServerRow } from './sql-server-row.ts';
6
+ export { SqlServerSession } from './sql-server-session.ts';
7
+ export { SqlServerTable } from './sql-server-table.ts';
8
+ export { OffsetTimestamp, type SqlServerValue, VariantValue, } from './values.ts';
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export { ChangeHistoryExpiredError, SqlServerLoginError, SqlServerPermissionError, SqlServerUnavailableError, SqlServerVersionError, } from './errors.js';
2
+ export { SqlServerColumn } from './sql-server-column.js';
3
+ export { SqlServerDatabase } from './sql-server-database.js';
4
+ export { SqlServerRow } from './sql-server-row.js';
5
+ export { SqlServerSession } from './sql-server-session.js';
6
+ export { SqlServerTable } from './sql-server-table.js';
7
+ export { OffsetTimestamp, VariantValue, } from './values.js';
@@ -0,0 +1 @@
1
+ export declare function quoteName(name: string): string;
@@ -0,0 +1,4 @@
1
+ // SQL Server's QUOTENAME: an identifier in brackets, any ] doubled.
2
+ export function quoteName(name) {
3
+ return `[${name.replaceAll(']', ']]')}]`;
4
+ }
@@ -0,0 +1,19 @@
1
+ import type { SqlServerCodec } from './codecs/sql-server-codec.ts';
2
+ import type { SqlServerValue } from './values.ts';
3
+ export declare class SqlServerColumn {
4
+ readonly name: string;
5
+ readonly type: string;
6
+ readonly nullable: boolean;
7
+ readonly masked: boolean;
8
+ readonly description: string | null;
9
+ readonly codec: SqlServerCodec<SqlServerValue>;
10
+ constructor(column: {
11
+ name: string;
12
+ type: string;
13
+ nullable: boolean;
14
+ masked: boolean;
15
+ description: string | null;
16
+ codec: SqlServerCodec<SqlServerValue>;
17
+ });
18
+ get quoted(): string;
19
+ }
@@ -0,0 +1,24 @@
1
+ import { quoteName } from './quote-name.js';
2
+ export class SqlServerColumn {
3
+ name;
4
+ // The type as the column declares it, such as decimal(19, 4).
5
+ type;
6
+ nullable;
7
+ // Dynamic data masking hides its values from a login without UNMASK.
8
+ masked;
9
+ // Its MS_Description extended property.
10
+ description;
11
+ codec;
12
+ constructor(column) {
13
+ this.name = column.name;
14
+ this.type = column.type;
15
+ this.nullable = column.nullable;
16
+ this.masked = column.masked;
17
+ this.description = column.description;
18
+ this.codec = column.codec;
19
+ Object.freeze(this);
20
+ }
21
+ get quoted() {
22
+ return quoteName(this.name);
23
+ }
24
+ }
@@ -0,0 +1,8 @@
1
+ import { SqlServerSession } from './sql-server-session.ts';
2
+ export declare class SqlServerDatabase {
3
+ #private;
4
+ readonly location: string;
5
+ readonly name: string;
6
+ constructor(connectionString: string);
7
+ open(): Promise<SqlServerSession>;
8
+ }
@@ -0,0 +1,65 @@
1
+ import sql from 'mssql';
2
+ import { cellNumber } from './cells.js';
3
+ import { SqlServerLoginError, SqlServerUnavailableError, SqlServerVersionError, } from './errors.js';
4
+ import { query } from './sql-server-request.js';
5
+ import { SqlServerSession } from './sql-server-session.js';
6
+ // One database on a SQL Server, named by an ADO.NET connection string
7
+ // (Server=host,port;Database=name;User Id=...;Password=...), the form SQL
8
+ // Server's own tools print.
9
+ export class SqlServerDatabase {
10
+ // Server and database, without credentials.
11
+ location;
12
+ // The database's own name, which stays when the server is reached another
13
+ // way.
14
+ name;
15
+ #config;
16
+ constructor(connectionString) {
17
+ const config = sql.ConnectionPool.parseConnectionString(connectionString);
18
+ // Without a database the login's default one opens, which the caller
19
+ // did not choose.
20
+ if (!config.server || !config.database)
21
+ throw new TypeError('A SQL Server connection string must name its Server and its Database');
22
+ const instance = config.options?.instanceName;
23
+ this.location = `${config.server}${instance ? `\\${instance}` : ''}:${config.port ?? 1433}/${config.database}`;
24
+ this.name = config.database;
25
+ // A read streams a whole table, so no time limit fits every table.
26
+ this.#config = { ...config, requestTimeout: 0, pool: { min: 0, max: 4 } };
27
+ Object.freeze(this);
28
+ }
29
+ async open() {
30
+ const pool = new sql.ConnectionPool(this.#config);
31
+ try {
32
+ await pool.connect();
33
+ }
34
+ catch (cause) {
35
+ throw refused(cause)
36
+ ? new SqlServerLoginError(this.location, cause)
37
+ : new SqlServerUnavailableError(this.location, cause);
38
+ }
39
+ 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()`);
41
+ const version = cellNumber(major, 'ProductMajorVersion');
42
+ // Azure SQL Database (5) and Managed Instance (8) report version 12
43
+ // but run the current engine.
44
+ if (edition !== 5 && edition !== 8 && version < 13)
45
+ throw new SqlServerVersionError(this.location, version);
46
+ return new SqlServerSession(pool, this.location, snapshot === 1);
47
+ }
48
+ catch (error) {
49
+ await pool.close();
50
+ throw error;
51
+ }
52
+ }
53
+ }
54
+ // Whether the server answered and refused the login. tedious marks the login
55
+ // errors SQL Server raises while it fails over or is busy (4060, 10928,
56
+ // 10929, 40197, 40501, 40613) as transient, and tries them again before it
57
+ // gives up; those mean the server cannot be reached now.
58
+ function refused(error) {
59
+ if (!(error instanceof sql.ConnectionError) || error.code !== 'ELOGIN')
60
+ return false;
61
+ const { originalError } = error;
62
+ return !(originalError !== undefined &&
63
+ 'isTransient' in originalError &&
64
+ originalError.isTransient === true);
65
+ }
@@ -0,0 +1,3 @@
1
+ import sql from 'mssql';
2
+ export declare function query(request: sql.Request, text: string, parameters?: readonly string[]): Promise<unknown[][]>;
3
+ export declare function stream(request: sql.Request, text: string, parameters?: readonly string[]): AsyncGenerator<unknown[]>;
@@ -0,0 +1,71 @@
1
+ import sql from 'mssql';
2
+ import { SqlServerPermissionError } from './errors.js';
3
+ // Every parameter travels as text and the statement casts it, so a value
4
+ // keeps the exact spelling a read gave it, whatever the driver would make of
5
+ // a number or a Date.
6
+ function bind(request, parameters) {
7
+ for (const [index, value] of parameters.entries())
8
+ request.input(`p${index}`, sql.NVarChar(sql.MAX), value);
9
+ }
10
+ // A refused read names the grant it needs (errors 229 and 230).
11
+ function readError(error) {
12
+ if (!(error instanceof sql.RequestError))
13
+ return error;
14
+ if (error.number !== 229 && error.number !== 230)
15
+ return error;
16
+ const denied = /^The (.+) permission was denied on the (?:column '(.+)' of the )?object '(.+)', database '.+', schema '(.+)'\.$/.exec(error.message);
17
+ if (denied === null)
18
+ return error;
19
+ const [, permission, column, object, schema] = denied;
20
+ const on = `[${schema}].[${object}]${column === undefined ? '' : ` ([${column}])`}`;
21
+ return new SqlServerPermissionError(`${permission} ON ${on}`, error);
22
+ }
23
+ function asRow(row) {
24
+ if (!Array.isArray(row))
25
+ throw new TypeError('SQL Server returned a row that is not a list');
26
+ return row;
27
+ }
28
+ // Every row of one statement, each as the values it selects in order.
29
+ export async function query(request, text, parameters = []) {
30
+ request.arrayRowMode = true;
31
+ bind(request, parameters);
32
+ try {
33
+ const { recordset } = await request.query(text);
34
+ return recordset.map(asRow);
35
+ }
36
+ catch (error) {
37
+ throw readError(error);
38
+ }
39
+ }
40
+ // The rows of one statement as the driver receives them; stopping early
41
+ // cancels the statement so its connection returns to the pool.
42
+ export async function* stream(request, text, parameters = []) {
43
+ request.arrayRowMode = true;
44
+ bind(request, parameters);
45
+ const rows = request.toReadableStream();
46
+ // The driver ends every statement, cancelled or failed too, with done.
47
+ const ended = new Promise((resolve) => request.once('done', () => resolve()));
48
+ let finished = false;
49
+ void ended.then(() => {
50
+ finished = true;
51
+ });
52
+ // In stream mode the promise always resolves; failures arrive on rows.
53
+ void request.query(text);
54
+ try {
55
+ for await (const row of rows) {
56
+ yield asRow(row);
57
+ }
58
+ }
59
+ catch (error) {
60
+ throw readError(error);
61
+ }
62
+ finally {
63
+ // A reader that stops early cancels the statement, and waits for it to
64
+ // end: until then the connection, or a transaction on it, is not free.
65
+ if (!finished) {
66
+ rows.on('error', () => { });
67
+ request.cancel();
68
+ await ended;
69
+ }
70
+ }
71
+ }
@@ -0,0 +1,10 @@
1
+ import type { SqlServerColumn } from './sql-server-column.ts';
2
+ import type { SqlServerTable } from './sql-server-table.ts';
3
+ import type { SqlServerValue } from './values.ts';
4
+ export declare class SqlServerRow {
5
+ readonly table: SqlServerTable;
6
+ readonly values: readonly SqlServerValue[];
7
+ constructor(table: SqlServerTable, values: readonly SqlServerValue[]);
8
+ value(column: SqlServerColumn): SqlServerValue;
9
+ get position(): readonly string[];
10
+ }