dbtour-source-mssql 0.2.4 → 0.4.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/map.d.ts +95 -0
- package/dist/map.js +159 -5
- package/dist/query.sql +212 -9
- package/package.json +2 -2
package/dist/map.d.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import type { Database } from 'dbtour';
|
|
2
|
+
/** node-mssql returns a bigint as a string, since it may not fit a JavaScript number. */
|
|
3
|
+
type Count = number | string;
|
|
4
|
+
/** Whether the sensitivity labels were read: the view is absent before SQL Server 2019, or the login may not. */
|
|
5
|
+
type Labels = 'read' | 'denied' | 'absent';
|
|
2
6
|
export interface InfoRow {
|
|
3
7
|
name: string;
|
|
4
8
|
version: string | null;
|
|
5
9
|
description: string | null;
|
|
10
|
+
readAt?: string | null;
|
|
11
|
+
collation?: string | null;
|
|
12
|
+
labels?: Labels | null;
|
|
6
13
|
}
|
|
7
14
|
export interface TableRow {
|
|
8
15
|
schema: string;
|
|
@@ -10,6 +17,11 @@ export interface TableRow {
|
|
|
10
17
|
kind: 'table' | 'view';
|
|
11
18
|
description: string | null;
|
|
12
19
|
definition: string | null;
|
|
20
|
+
rows?: Count | null;
|
|
21
|
+
dataBytes?: Count | null;
|
|
22
|
+
indexBytes?: Count | null;
|
|
23
|
+
created?: string | null;
|
|
24
|
+
altered?: string | null;
|
|
13
25
|
}
|
|
14
26
|
export interface ColumnRow {
|
|
15
27
|
schema: string;
|
|
@@ -25,6 +37,10 @@ export interface ColumnRow {
|
|
|
25
37
|
default: string | null;
|
|
26
38
|
computed: string | null;
|
|
27
39
|
description: string | null;
|
|
40
|
+
collation?: string | null;
|
|
41
|
+
sparse?: boolean | null;
|
|
42
|
+
identitySeed?: string | null;
|
|
43
|
+
identityIncrement?: string | null;
|
|
28
44
|
}
|
|
29
45
|
export interface KeyRow {
|
|
30
46
|
schema: string;
|
|
@@ -45,6 +61,8 @@ export interface ForeignKeyRow {
|
|
|
45
61
|
ordinal: number;
|
|
46
62
|
onDelete: string;
|
|
47
63
|
onUpdate: string;
|
|
64
|
+
disabled?: boolean | null;
|
|
65
|
+
untrusted?: boolean | null;
|
|
48
66
|
}
|
|
49
67
|
export interface IndexRow {
|
|
50
68
|
schema: string;
|
|
@@ -55,6 +73,9 @@ export interface IndexRow {
|
|
|
55
73
|
column: string;
|
|
56
74
|
ordinal: number;
|
|
57
75
|
included: boolean;
|
|
76
|
+
type?: string | null;
|
|
77
|
+
filter?: string | null;
|
|
78
|
+
disabled?: boolean | null;
|
|
58
79
|
}
|
|
59
80
|
export interface CheckRow {
|
|
60
81
|
schema: string;
|
|
@@ -62,6 +83,73 @@ export interface CheckRow {
|
|
|
62
83
|
name: string;
|
|
63
84
|
definition: string;
|
|
64
85
|
column: string | null;
|
|
86
|
+
disabled?: boolean | null;
|
|
87
|
+
untrusted?: boolean | null;
|
|
88
|
+
}
|
|
89
|
+
/** An extended property and what it is on: `item` is the column, index or constraint. */
|
|
90
|
+
export interface PropertyRow {
|
|
91
|
+
on: 'database' | 'schema' | 'table' | 'column' | 'index' | 'constraint';
|
|
92
|
+
schema: string | null;
|
|
93
|
+
table: string | null;
|
|
94
|
+
item: string | null;
|
|
95
|
+
name: string;
|
|
96
|
+
value: string | null;
|
|
97
|
+
}
|
|
98
|
+
export interface FeatureRow {
|
|
99
|
+
schema: string;
|
|
100
|
+
table: string;
|
|
101
|
+
/** 0 none, 1 a history table, 2 system-versioned. */
|
|
102
|
+
temporalType: number | null;
|
|
103
|
+
historySchema: string | null;
|
|
104
|
+
historyTable: string | null;
|
|
105
|
+
periodStart: string | null;
|
|
106
|
+
periodEnd: string | null;
|
|
107
|
+
memoryOptimised: boolean | null;
|
|
108
|
+
durability: string | null;
|
|
109
|
+
ledger: string | null;
|
|
110
|
+
cdc: boolean | null;
|
|
111
|
+
changeTracking: number | boolean | null;
|
|
112
|
+
scheme: string | null;
|
|
113
|
+
function: string | null;
|
|
114
|
+
partitionColumn: string | null;
|
|
115
|
+
partitions: number | null;
|
|
116
|
+
compression: string | null;
|
|
117
|
+
}
|
|
118
|
+
/** One row per trigger and event it fires on. */
|
|
119
|
+
export interface TriggerRow {
|
|
120
|
+
schema: string;
|
|
121
|
+
table: string;
|
|
122
|
+
name: string;
|
|
123
|
+
insteadOf: boolean;
|
|
124
|
+
disabled: boolean;
|
|
125
|
+
event: string;
|
|
126
|
+
definition: string | null;
|
|
127
|
+
}
|
|
128
|
+
export interface SecurityRow {
|
|
129
|
+
schema: string;
|
|
130
|
+
table: string;
|
|
131
|
+
policySchema: string;
|
|
132
|
+
policy: string;
|
|
133
|
+
enabled: boolean;
|
|
134
|
+
kind: string;
|
|
135
|
+
operation: string | null;
|
|
136
|
+
predicate: string;
|
|
137
|
+
}
|
|
138
|
+
export interface ColumnSecurityRow {
|
|
139
|
+
schema: string;
|
|
140
|
+
table: string;
|
|
141
|
+
column: string;
|
|
142
|
+
masked: string | null;
|
|
143
|
+
encryption: string | null;
|
|
144
|
+
key: string | null;
|
|
145
|
+
}
|
|
146
|
+
export interface SensitivityRow {
|
|
147
|
+
schema: string;
|
|
148
|
+
table: string;
|
|
149
|
+
column: string;
|
|
150
|
+
label: string | null;
|
|
151
|
+
informationType: string | null;
|
|
152
|
+
rank: string | null;
|
|
65
153
|
}
|
|
66
154
|
export interface Recordsets {
|
|
67
155
|
info: InfoRow[];
|
|
@@ -71,6 +159,12 @@ export interface Recordsets {
|
|
|
71
159
|
foreignKeys: ForeignKeyRow[];
|
|
72
160
|
indexes: IndexRow[];
|
|
73
161
|
checks: CheckRow[];
|
|
162
|
+
properties: PropertyRow[];
|
|
163
|
+
features: FeatureRow[];
|
|
164
|
+
triggers: TriggerRow[];
|
|
165
|
+
security: SecurityRow[];
|
|
166
|
+
columnSecurity: ColumnSecurityRow[];
|
|
167
|
+
sensitivity: SensitivityRow[];
|
|
74
168
|
}
|
|
75
169
|
export interface MapOptions {
|
|
76
170
|
/** The name the docs call the database; the server's own name when not given. */
|
|
@@ -83,3 +177,4 @@ export declare function toRecordsets(sets: unknown[][]): Recordsets;
|
|
|
83
177
|
export declare function mapRecordsets(sets: Recordsets, options?: MapOptions): Database;
|
|
84
178
|
/** The type as SQL Server would spell it in a CREATE TABLE: nvarchar(320), varchar(max), decimal(18,2), datetime2(7). */
|
|
85
179
|
export declare function formatType(row: Pick<ColumnRow, 'type' | 'maxLength' | 'precision' | 'scale'>): string;
|
|
180
|
+
export {};
|
package/dist/map.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/** The result sets in the order query.sql returns them. */
|
|
2
2
|
export function toRecordsets(sets) {
|
|
3
|
-
if (sets.length !==
|
|
4
|
-
throw new Error(`query.sql returns
|
|
5
|
-
const [info, tables, columns, keys, foreignKeys, indexes, checks] = sets;
|
|
6
|
-
return { info, tables, columns, keys, foreignKeys, indexes, checks };
|
|
3
|
+
if (sets.length !== 13)
|
|
4
|
+
throw new Error(`query.sql returns 13 result sets, got ${sets.length}: the query and map.ts disagree`);
|
|
5
|
+
const [info, tables, columns, keys, foreignKeys, indexes, checks, properties, features, triggers, security, columnSecurity, sensitivity] = sets;
|
|
6
|
+
return { info, tables, columns, keys, foreignKeys, indexes, checks, properties, features, triggers, security, columnSecurity, sensitivity };
|
|
7
7
|
}
|
|
8
8
|
export function mapRecordsets(sets, options = {}) {
|
|
9
9
|
const info = sets.info[0];
|
|
@@ -22,6 +22,14 @@ export function mapRecordsets(sets, options = {}) {
|
|
|
22
22
|
table.description = row.description;
|
|
23
23
|
if (row.kind === 'view' && row.definition)
|
|
24
24
|
table.definition = row.definition;
|
|
25
|
+
// A byte count past 2^53 loses its last digits as a number, which no reader of a size will miss. Only a
|
|
26
|
+
// table: an indexed view has rows in sys.partitions too, but they are the view's results, not stored data.
|
|
27
|
+
if (row.kind === 'table' && row.rows !== null && row.rows !== undefined)
|
|
28
|
+
table.stats = { rows: Number(row.rows), dataBytes: Number(row.dataBytes ?? 0), indexBytes: Number(row.indexBytes ?? 0) };
|
|
29
|
+
if (row.created)
|
|
30
|
+
table.created = row.created;
|
|
31
|
+
if (row.altered)
|
|
32
|
+
table.altered = row.altered;
|
|
25
33
|
schema.tables.push(table);
|
|
26
34
|
tables.set(`${row.schema}.${row.name}`, table);
|
|
27
35
|
}
|
|
@@ -36,8 +44,17 @@ export function mapRecordsets(sets, options = {}) {
|
|
|
36
44
|
column.computed = row.computed;
|
|
37
45
|
if (row.identity)
|
|
38
46
|
column.identity = true;
|
|
47
|
+
if (row.identitySeed != null)
|
|
48
|
+
column.identitySeed = row.identitySeed; // eslint-disable-line eqeqeq
|
|
49
|
+
if (row.identityIncrement != null)
|
|
50
|
+
column.identityIncrement = row.identityIncrement; // eslint-disable-line eqeqeq
|
|
39
51
|
if (row.description)
|
|
40
52
|
column.description = row.description;
|
|
53
|
+
// The database's collation is said once, on the database; a column says its own only when it differs.
|
|
54
|
+
if (row.collation && row.collation !== info.collation)
|
|
55
|
+
column.collation = row.collation;
|
|
56
|
+
if (row.sparse)
|
|
57
|
+
column.sparse = true;
|
|
41
58
|
table.columns.push(column);
|
|
42
59
|
}
|
|
43
60
|
for (const [key, rows] of groupBy(sets.keys, (r) => `${r.schema}.${r.table}|${r.name}`)) {
|
|
@@ -62,8 +79,13 @@ export function mapRecordsets(sets, options = {}) {
|
|
|
62
79
|
relation.onDelete = first.onDelete;
|
|
63
80
|
if (first.onUpdate && first.onUpdate !== 'NO_ACTION')
|
|
64
81
|
relation.onUpdate = first.onUpdate;
|
|
82
|
+
const constraint = { name: first.name, kind: 'foreign', columns: relation.from.columns, definition: `${to} (${relation.to.columns.join(', ')})` };
|
|
83
|
+
if (first.disabled)
|
|
84
|
+
relation.disabled = constraint.disabled = true;
|
|
85
|
+
if (first.untrusted)
|
|
86
|
+
relation.untrusted = constraint.untrusted = true;
|
|
65
87
|
relations.push(relation);
|
|
66
|
-
tables.get(from).constraints.push(
|
|
88
|
+
tables.get(from).constraints.push(constraint);
|
|
67
89
|
}
|
|
68
90
|
for (const [key, rows] of groupBy(sets.indexes, (r) => `${r.schema}.${r.table}|${r.name}`)) {
|
|
69
91
|
const table = tables.get(key.split('|')[0]);
|
|
@@ -77,6 +99,12 @@ export function mapRecordsets(sets, options = {}) {
|
|
|
77
99
|
const included = rows.filter((r) => r.included).map((r) => r.column);
|
|
78
100
|
if (included.length > 0)
|
|
79
101
|
index.included = included;
|
|
102
|
+
if (rows[0].type)
|
|
103
|
+
index.type = rows[0].type.toLowerCase().replace(/_/g, ' ');
|
|
104
|
+
if (rows[0].filter)
|
|
105
|
+
index.filter = rows[0].filter;
|
|
106
|
+
if (rows[0].disabled)
|
|
107
|
+
index.disabled = true;
|
|
80
108
|
table.indexes.push(index);
|
|
81
109
|
}
|
|
82
110
|
for (const row of sets.checks) {
|
|
@@ -84,15 +112,141 @@ export function mapRecordsets(sets, options = {}) {
|
|
|
84
112
|
if (!table)
|
|
85
113
|
continue;
|
|
86
114
|
const constraint = { name: row.name, kind: 'check', columns: row.column ? [row.column] : [], definition: row.definition };
|
|
115
|
+
if (row.disabled)
|
|
116
|
+
constraint.disabled = true;
|
|
117
|
+
if (row.untrusted)
|
|
118
|
+
constraint.untrusted = true;
|
|
87
119
|
table.constraints.push(constraint);
|
|
88
120
|
}
|
|
121
|
+
for (const row of sets.features ?? []) {
|
|
122
|
+
const table = tables.get(`${row.schema}.${row.table}`);
|
|
123
|
+
if (table)
|
|
124
|
+
addFeatures(table, row, tables);
|
|
125
|
+
}
|
|
126
|
+
for (const [key, rows] of groupBy(sets.triggers ?? [], (r) => `${r.schema}.${r.table}|${r.name}`)) {
|
|
127
|
+
const table = tables.get(key.split('|')[0]);
|
|
128
|
+
if (!table)
|
|
129
|
+
continue;
|
|
130
|
+
const trigger = { name: rows[0].name, timing: rows[0].insteadOf ? 'instead of' : 'after', events: rows.map((r) => r.event.toLowerCase()), enabled: !rows[0].disabled };
|
|
131
|
+
if (rows[0].definition)
|
|
132
|
+
trigger.definition = rows[0].definition;
|
|
133
|
+
(table.triggers ??= []).push(trigger);
|
|
134
|
+
}
|
|
135
|
+
for (const row of sets.security ?? []) {
|
|
136
|
+
const table = tables.get(`${row.schema}.${row.table}`);
|
|
137
|
+
if (!table)
|
|
138
|
+
continue;
|
|
139
|
+
const predicate = { policy: `${row.policySchema}.${row.policy}`, kind: row.kind === 'BLOCK' ? 'block' : 'filter', predicate: row.predicate, enabled: Boolean(row.enabled) };
|
|
140
|
+
if (row.operation)
|
|
141
|
+
predicate.operation = row.operation.toLowerCase();
|
|
142
|
+
(table.security ??= []).push(predicate);
|
|
143
|
+
}
|
|
144
|
+
const columnOf = (schema, table, column) => tables.get(`${schema}.${table}`)?.columns.find((c) => c.name === column);
|
|
145
|
+
for (const row of sets.columnSecurity ?? []) {
|
|
146
|
+
const column = columnOf(row.schema, row.table, row.column);
|
|
147
|
+
if (!column)
|
|
148
|
+
continue;
|
|
149
|
+
if (row.masked)
|
|
150
|
+
column.masked = row.masked;
|
|
151
|
+
if (row.encryption && row.key)
|
|
152
|
+
column.encrypted = { type: row.encryption === 'RANDOMIZED' ? 'randomized' : 'deterministic', key: row.key };
|
|
153
|
+
}
|
|
154
|
+
for (const row of sets.sensitivity ?? []) {
|
|
155
|
+
const column = columnOf(row.schema, row.table, row.column);
|
|
156
|
+
if (!column)
|
|
157
|
+
continue;
|
|
158
|
+
const sensitivity = {};
|
|
159
|
+
if (row.label)
|
|
160
|
+
sensitivity.label = row.label;
|
|
161
|
+
if (row.informationType)
|
|
162
|
+
sensitivity.informationType = row.informationType;
|
|
163
|
+
if (row.rank && row.rank !== 'NONE')
|
|
164
|
+
sensitivity.rank = row.rank.toLowerCase();
|
|
165
|
+
if (Object.keys(sensitivity).length > 0)
|
|
166
|
+
column.sensitivity = sensitivity;
|
|
167
|
+
}
|
|
89
168
|
const database = { name: options.name ?? info.name, driver: 'mssql', schemas, relations };
|
|
90
169
|
if (info.version)
|
|
91
170
|
database.version = info.version;
|
|
92
171
|
if (info.description)
|
|
93
172
|
database.description = info.description;
|
|
173
|
+
if (info.collation)
|
|
174
|
+
database.collation = info.collation;
|
|
175
|
+
addProperties(sets.properties ?? [], database, tables);
|
|
176
|
+
if (info.labels === 'denied')
|
|
177
|
+
database.unread = ['sensitivity labels: the login lacks VIEW ANY SENSITIVITY CLASSIFICATION'];
|
|
178
|
+
if (info.readAt && [...tables.values()].some((t) => t.stats))
|
|
179
|
+
database.statsReadAt = info.readAt;
|
|
94
180
|
return database;
|
|
95
181
|
}
|
|
182
|
+
/** What sys.tables says beyond the structure; a feature the server lacks arrives as NULL and is left out. */
|
|
183
|
+
function addFeatures(table, row, tables) {
|
|
184
|
+
if (row.temporalType === 2 && row.historySchema && row.historyTable && row.periodStart && row.periodEnd) {
|
|
185
|
+
const history = `${row.historySchema}.${row.historyTable}`;
|
|
186
|
+
table.temporal = { history, start: row.periodStart, end: row.periodEnd };
|
|
187
|
+
const kept = tables.get(history);
|
|
188
|
+
if (kept)
|
|
189
|
+
kept.historyOf = `${row.schema}.${row.table}`;
|
|
190
|
+
}
|
|
191
|
+
if (row.memoryOptimised)
|
|
192
|
+
table.memoryOptimised = row.durability === 'SCHEMA_ONLY' ? 'schema only' : 'schema and data';
|
|
193
|
+
const ledgers = { UPDATABLE_LEDGER_TABLE: 'updatable', APPEND_ONLY_LEDGER_TABLE: 'append-only', HISTORY_TABLE: 'history' };
|
|
194
|
+
if (row.ledger && ledgers[row.ledger])
|
|
195
|
+
table.ledger = ledgers[row.ledger];
|
|
196
|
+
if (row.scheme && row.function && row.partitionColumn) {
|
|
197
|
+
table.partitioning = { scheme: row.scheme, function: row.function, column: row.partitionColumn, partitions: Number(row.partitions ?? 0) };
|
|
198
|
+
}
|
|
199
|
+
// Every partition's setting, comma-separated: one kind is the table's, several are said as a list.
|
|
200
|
+
const kinds = [...new Set((row.compression ?? '').split(',').map((k) => k.trim().toLowerCase().replace(/_/g, ' ')).filter(Boolean))].sort();
|
|
201
|
+
if (kinds.length > 1)
|
|
202
|
+
table.compression = kinds.join(', ');
|
|
203
|
+
else if (kinds.length === 1 && kinds[0] !== 'none')
|
|
204
|
+
table.compression = kinds[0];
|
|
205
|
+
if (row.changeTracking)
|
|
206
|
+
table.changeTracking = true;
|
|
207
|
+
if (row.cdc)
|
|
208
|
+
table.cdc = true;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Extended properties onto what they describe. MS_Description is the description of a database, table or
|
|
212
|
+
* column, read apart; on a schema, index or constraint, which have no description, it stays a property.
|
|
213
|
+
*/
|
|
214
|
+
function addProperties(rows, database, tables) {
|
|
215
|
+
for (const row of rows) {
|
|
216
|
+
if (row.value === null || row.value === undefined)
|
|
217
|
+
continue;
|
|
218
|
+
if (row.name === 'MS_Description' && (row.on === 'database' || row.on === 'table' || row.on === 'column'))
|
|
219
|
+
continue;
|
|
220
|
+
const table = row.table ? tables.get(`${row.schema}.${row.table}`) : undefined;
|
|
221
|
+
let owner;
|
|
222
|
+
if (row.on === 'database')
|
|
223
|
+
owner = database;
|
|
224
|
+
else if (row.on === 'schema')
|
|
225
|
+
owner = database.schemas.find((s) => s.name === row.schema);
|
|
226
|
+
else if (row.on === 'table')
|
|
227
|
+
owner = table;
|
|
228
|
+
else if (row.on === 'column')
|
|
229
|
+
owner = table?.columns.find((c) => c.name === row.item);
|
|
230
|
+
else if (row.on === 'index')
|
|
231
|
+
owner = table?.indexes.find((i) => i.name === row.item);
|
|
232
|
+
else
|
|
233
|
+
owner = table?.constraints.find((c) => c.name === row.item);
|
|
234
|
+
// A primary key's index is not among the indexes: the constraint stands for it, and keeps its properties.
|
|
235
|
+
// One the constraint already has under the same name, with another value, is kept beside it.
|
|
236
|
+
let name = row.name;
|
|
237
|
+
if (!owner && row.on === 'index') {
|
|
238
|
+
owner = table?.constraints.find((c) => c.kind === 'primary' && c.name === row.item);
|
|
239
|
+
if (owner?.properties && Object.hasOwn(owner.properties, name) && owner.properties[name] !== row.value)
|
|
240
|
+
name = `${name} (index)`;
|
|
241
|
+
}
|
|
242
|
+
if (owner)
|
|
243
|
+
setProperty((owner.properties ??= {}), name, row.value);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/** As an own property, so a catalogue name that is also a name of Object's own (__proto__) is kept like any. */
|
|
247
|
+
function setProperty(properties, name, value) {
|
|
248
|
+
Object.defineProperty(properties, name, { value, enumerable: true, writable: true, configurable: true });
|
|
249
|
+
}
|
|
96
250
|
/** The type as SQL Server would spell it in a CREATE TABLE: nvarchar(320), varchar(max), decimal(18,2), datetime2(7). */
|
|
97
251
|
export function formatType(row) {
|
|
98
252
|
const t = row.type.toLowerCase();
|
package/dist/query.sql
CHANGED
|
@@ -1,22 +1,61 @@
|
|
|
1
|
-
-- Everything dbtour needs from SQL Server, in one batch of
|
|
2
|
-
-- Reads the catalogue views only: a login with VIEW DEFINITION on the database is enough.
|
|
1
|
+
-- Everything dbtour needs from SQL Server, in one batch of thirteen result sets, read in this order by map.ts.
|
|
2
|
+
-- Reads the catalogue views only: a login with VIEW DEFINITION on the database is enough. That is why the
|
|
3
|
+
-- sizes come from sys.partitions and sys.allocation_units, catalogue views, rather than
|
|
4
|
+
-- sys.dm_db_partition_stats, which wants VIEW DATABASE STATE and would fail the whole batch without it.
|
|
5
|
+
-- What only newer servers keep (temporal 2016, masking and Always Encrypted 2016, sensitivity labels 2019,
|
|
6
|
+
-- ledger 2022) is read through text the batch assembles after asking whether it exists, so an older server
|
|
7
|
+
-- returns NULL or an empty set there instead of failing. The labels alone want more than VIEW DEFINITION
|
|
8
|
+
-- (VIEW ANY SENSITIVITY CLASSIFICATION); set 1 says whether they were read.
|
|
3
9
|
SET NOCOUNT ON;
|
|
4
10
|
|
|
5
11
|
-- 1. The database
|
|
6
12
|
SELECT DB_NAME() AS [name],
|
|
7
13
|
CAST(SERVERPROPERTY('ProductVersion') AS nvarchar(50)) AS [version],
|
|
8
|
-
(SELECT TOP (1) CAST([value] AS nvarchar(max)) FROM sys.extended_properties WHERE class = 0 AND [name] = 'MS_Description') AS [description]
|
|
14
|
+
(SELECT TOP (1) CAST([value] AS nvarchar(max)) FROM sys.extended_properties WHERE class = 0 AND [name] = 'MS_Description') AS [description],
|
|
15
|
+
CONVERT(nvarchar(33), SYSUTCDATETIME(), 127) + 'Z' AS [readAt],
|
|
16
|
+
CAST(DATABASEPROPERTYEX(DB_NAME(), 'Collation') AS nvarchar(128)) AS [collation],
|
|
17
|
+
CASE WHEN OBJECT_ID('sys.sensitivity_classifications') IS NULL THEN 'absent'
|
|
18
|
+
WHEN HAS_PERMS_BY_NAME(DB_NAME(), 'DATABASE', 'VIEW ANY SENSITIVITY CLASSIFICATION') = 1 THEN 'read'
|
|
19
|
+
ELSE 'denied' END AS [labels];
|
|
9
20
|
|
|
10
|
-
-- 2. Tables and views
|
|
21
|
+
-- 2. Tables and views, with each table's rows and bytes as the catalogue counts them (no row is read).
|
|
22
|
+
-- Rows are the heap's or the clustered index's (index_id 0 or 1); data bytes are those pages, index bytes
|
|
23
|
+
-- every other index's. A page is 8 KB. A view has no partitions, so its three are NULL.
|
|
11
24
|
SELECT s.[name] AS [schema],
|
|
12
25
|
o.[name] AS [name],
|
|
13
26
|
CASE o.[type] WHEN 'V' THEN 'view' ELSE 'table' END AS [kind],
|
|
14
27
|
CAST(ep.[value] AS nvarchar(max)) AS [description],
|
|
15
|
-
m.[definition] AS [definition]
|
|
28
|
+
m.[definition] AS [definition],
|
|
29
|
+
-- The server's clock, which is not always UTC: the date is what a reader wants.
|
|
30
|
+
CONVERT(nvarchar(30), o.create_date, 126) AS [created],
|
|
31
|
+
CONVERT(nvarchar(30), o.modify_date, 126) AS [altered],
|
|
32
|
+
r.[rows] AS [rows],
|
|
33
|
+
b.dataBytes AS [dataBytes],
|
|
34
|
+
b.indexBytes AS [indexBytes]
|
|
16
35
|
FROM sys.objects o
|
|
17
36
|
JOIN sys.schemas s ON s.schema_id = o.schema_id
|
|
18
37
|
LEFT JOIN sys.extended_properties ep ON ep.class = 1 AND ep.major_id = o.object_id AND ep.minor_id = 0 AND ep.[name] = 'MS_Description'
|
|
19
38
|
LEFT JOIN sys.sql_modules m ON m.object_id = o.object_id
|
|
39
|
+
-- Counted apart: joining the allocation units to the partitions repeats each partition's rows.
|
|
40
|
+
OUTER APPLY (SELECT SUM(p.[rows]) AS [rows] FROM sys.partitions p WHERE p.object_id = o.object_id AND p.index_id IN (0, 1)) r
|
|
41
|
+
OUTER APPLY (
|
|
42
|
+
SELECT SUM(CASE WHEN u.index_id IN (0, 1) THEN u.used_pages ELSE 0 END) * 8192 AS dataBytes,
|
|
43
|
+
SUM(CASE WHEN u.index_id > 1 THEN u.used_pages ELSE 0 END) * 8192 AS indexBytes
|
|
44
|
+
FROM (
|
|
45
|
+
-- In-row and row-overflow pages belong to the partition's hobt, large-object pages to the partition.
|
|
46
|
+
SELECT p.index_id, au.used_pages
|
|
47
|
+
FROM sys.partitions p
|
|
48
|
+
JOIN sys.allocation_units au ON au.container_id = CASE WHEN au.[type] = 2 THEN p.partition_id ELSE p.hobt_id END
|
|
49
|
+
WHERE p.object_id = o.object_id
|
|
50
|
+
UNION ALL
|
|
51
|
+
-- A columnstore's delta stores, rows not yet compressed, are hobts of their own that sys.partitions
|
|
52
|
+
-- does not list: their pages are counted with the columnstore index they belong to.
|
|
53
|
+
SELECT rg.index_id, au.used_pages
|
|
54
|
+
FROM sys.column_store_row_groups rg
|
|
55
|
+
JOIN sys.allocation_units au ON au.container_id = rg.delta_store_hobt_id
|
|
56
|
+
WHERE rg.object_id = o.object_id AND rg.delta_store_hobt_id IS NOT NULL
|
|
57
|
+
) u
|
|
58
|
+
) b
|
|
20
59
|
WHERE o.[type] IN ('U', 'V') AND o.is_ms_shipped = 0
|
|
21
60
|
ORDER BY s.[name], o.[name];
|
|
22
61
|
|
|
@@ -33,7 +72,11 @@ SELECT s.[name] AS [schema],
|
|
|
33
72
|
c.is_identity AS [identity],
|
|
34
73
|
d.[definition] AS [default],
|
|
35
74
|
cc.[definition] AS [computed],
|
|
36
|
-
CAST(ep.[value] AS nvarchar(max)) AS [description]
|
|
75
|
+
CAST(ep.[value] AS nvarchar(max)) AS [description],
|
|
76
|
+
c.collation_name AS [collation],
|
|
77
|
+
c.is_sparse AS [sparse],
|
|
78
|
+
CAST(idc.seed_value AS nvarchar(40)) AS [identitySeed],
|
|
79
|
+
CAST(idc.increment_value AS nvarchar(40)) AS [identityIncrement]
|
|
37
80
|
FROM sys.columns c
|
|
38
81
|
JOIN sys.objects o ON o.object_id = c.object_id AND o.[type] IN ('U', 'V') AND o.is_ms_shipped = 0
|
|
39
82
|
JOIN sys.schemas s ON s.schema_id = o.schema_id
|
|
@@ -41,6 +84,7 @@ JOIN sys.types t ON t.user_type_id = c.user_type_id
|
|
|
41
84
|
LEFT JOIN sys.default_constraints d ON d.object_id = c.default_object_id
|
|
42
85
|
LEFT JOIN sys.computed_columns cc ON cc.object_id = c.object_id AND cc.column_id = c.column_id
|
|
43
86
|
LEFT JOIN sys.extended_properties ep ON ep.class = 1 AND ep.major_id = c.object_id AND ep.minor_id = c.column_id AND ep.[name] = 'MS_Description'
|
|
87
|
+
LEFT JOIN sys.identity_columns idc ON idc.object_id = c.object_id AND idc.column_id = c.column_id
|
|
44
88
|
ORDER BY s.[name], o.[name], c.column_id;
|
|
45
89
|
|
|
46
90
|
-- 4. Primary keys and unique constraints, one row per column
|
|
@@ -67,7 +111,9 @@ SELECT fk.[name] AS [name],
|
|
|
67
111
|
rc.[name] AS [toColumn],
|
|
68
112
|
fkc.constraint_column_id AS [ordinal],
|
|
69
113
|
fk.delete_referential_action_desc AS [onDelete],
|
|
70
|
-
fk.update_referential_action_desc AS [onUpdate]
|
|
114
|
+
fk.update_referential_action_desc AS [onUpdate],
|
|
115
|
+
fk.is_disabled AS [disabled],
|
|
116
|
+
fk.is_not_trusted AS [untrusted]
|
|
71
117
|
FROM sys.foreign_keys fk
|
|
72
118
|
JOIN sys.foreign_key_columns fkc ON fkc.constraint_object_id = fk.object_id
|
|
73
119
|
JOIN sys.objects po ON po.object_id = fk.parent_object_id
|
|
@@ -86,7 +132,10 @@ SELECT s.[name] AS [schema],
|
|
|
86
132
|
i.is_primary_key AS [primary],
|
|
87
133
|
c.[name] AS [column],
|
|
88
134
|
ic.key_ordinal AS [ordinal],
|
|
89
|
-
ic.is_included_column AS [included]
|
|
135
|
+
ic.is_included_column AS [included],
|
|
136
|
+
i.type_desc AS [type],
|
|
137
|
+
i.filter_definition AS [filter],
|
|
138
|
+
i.is_disabled AS [disabled]
|
|
90
139
|
FROM sys.indexes i
|
|
91
140
|
JOIN sys.objects o ON o.object_id = i.object_id AND o.[type] IN ('U', 'V') AND o.is_ms_shipped = 0
|
|
92
141
|
JOIN sys.schemas s ON s.schema_id = o.schema_id
|
|
@@ -100,9 +149,163 @@ SELECT s.[name] AS [schema],
|
|
|
100
149
|
o.[name] AS [table],
|
|
101
150
|
ck.[name] AS [name],
|
|
102
151
|
ck.[definition] AS [definition],
|
|
103
|
-
c.[name] AS [column]
|
|
152
|
+
c.[name] AS [column],
|
|
153
|
+
ck.is_disabled AS [disabled],
|
|
154
|
+
ck.is_not_trusted AS [untrusted]
|
|
104
155
|
FROM sys.check_constraints ck
|
|
105
156
|
JOIN sys.objects o ON o.object_id = ck.parent_object_id AND o.is_ms_shipped = 0
|
|
106
157
|
JOIN sys.schemas s ON s.schema_id = o.schema_id
|
|
107
158
|
LEFT JOIN sys.columns c ON c.object_id = ck.parent_object_id AND c.column_id = ck.parent_column_id
|
|
108
159
|
ORDER BY s.[name], o.[name], ck.[name];
|
|
160
|
+
|
|
161
|
+
-- 8. Extended properties, every name, on the database, schemas, tables, columns, constraints and indexes.
|
|
162
|
+
-- MS_Description comes along too; map.ts keeps it apart where it is already the description.
|
|
163
|
+
SELECT CAST('database' AS varchar(10)) AS [on], CAST(NULL AS sysname) AS [schema], CAST(NULL AS sysname) AS [table],
|
|
164
|
+
CAST(NULL AS sysname) AS [item], ep.[name] AS [name], CAST(ep.[value] AS nvarchar(max)) AS [value]
|
|
165
|
+
FROM sys.extended_properties ep
|
|
166
|
+
WHERE ep.class = 0
|
|
167
|
+
UNION ALL
|
|
168
|
+
SELECT 'schema', s.[name], NULL, NULL, ep.[name], CAST(ep.[value] AS nvarchar(max))
|
|
169
|
+
FROM sys.extended_properties ep
|
|
170
|
+
JOIN sys.schemas s ON s.schema_id = ep.major_id
|
|
171
|
+
WHERE ep.class = 3
|
|
172
|
+
UNION ALL
|
|
173
|
+
SELECT CASE WHEN ep.minor_id = 0 THEN 'table' ELSE 'column' END, s.[name], o.[name], c.[name], ep.[name], CAST(ep.[value] AS nvarchar(max))
|
|
174
|
+
FROM sys.extended_properties ep
|
|
175
|
+
JOIN sys.objects o ON o.object_id = ep.major_id AND o.[type] IN ('U', 'V') AND o.is_ms_shipped = 0
|
|
176
|
+
JOIN sys.schemas s ON s.schema_id = o.schema_id
|
|
177
|
+
LEFT JOIN sys.columns c ON c.object_id = ep.major_id AND c.column_id = ep.minor_id
|
|
178
|
+
WHERE ep.class = 1
|
|
179
|
+
UNION ALL
|
|
180
|
+
SELECT 'constraint', s.[name], t.[name], k.[name], ep.[name], CAST(ep.[value] AS nvarchar(max))
|
|
181
|
+
FROM sys.extended_properties ep
|
|
182
|
+
JOIN sys.objects k ON k.object_id = ep.major_id AND k.[type] IN ('PK', 'UQ', 'F', 'C')
|
|
183
|
+
JOIN sys.objects t ON t.object_id = k.parent_object_id AND t.is_ms_shipped = 0
|
|
184
|
+
JOIN sys.schemas s ON s.schema_id = t.schema_id
|
|
185
|
+
WHERE ep.class = 1 AND ep.minor_id = 0
|
|
186
|
+
UNION ALL
|
|
187
|
+
SELECT 'index', s.[name], o.[name], i.[name], ep.[name], CAST(ep.[value] AS nvarchar(max))
|
|
188
|
+
FROM sys.extended_properties ep
|
|
189
|
+
JOIN sys.indexes i ON i.object_id = ep.major_id AND i.index_id = ep.minor_id
|
|
190
|
+
JOIN sys.objects o ON o.object_id = i.object_id AND o.is_ms_shipped = 0
|
|
191
|
+
JOIN sys.schemas s ON s.schema_id = o.schema_id
|
|
192
|
+
WHERE ep.class = 7;
|
|
193
|
+
|
|
194
|
+
-- 9. What else the catalogue says about each table: temporal, memory-optimised, ledger, partitioned,
|
|
195
|
+
-- compressed, change tracking, change data capture. Assembled as text: a column the server is too old
|
|
196
|
+
-- to have is read as NULL. The literal starts as nvarchar(max) so the text is never cut at 4,000.
|
|
197
|
+
DECLARE @temporal bit = CASE WHEN COL_LENGTH('sys.tables', 'temporal_type') IS NOT NULL THEN 1 ELSE 0 END;
|
|
198
|
+
DECLARE @features nvarchar(max) = CAST(N'' AS nvarchar(max)) + N'
|
|
199
|
+
SELECT s.[name] AS [schema],
|
|
200
|
+
t.[name] AS [table],
|
|
201
|
+
' + CASE WHEN @temporal = 1 THEN N't.temporal_type' ELSE N'CAST(NULL AS tinyint)' END + N' AS [temporalType],
|
|
202
|
+
' + CASE WHEN @temporal = 1 THEN N'OBJECT_SCHEMA_NAME(t.history_table_id)' ELSE N'CAST(NULL AS sysname)' END + N' AS [historySchema],
|
|
203
|
+
' + CASE WHEN @temporal = 1 THEN N'OBJECT_NAME(t.history_table_id)' ELSE N'CAST(NULL AS sysname)' END + N' AS [historyTable],
|
|
204
|
+
' + CASE WHEN @temporal = 1
|
|
205
|
+
THEN N'(SELECT c.[name] FROM sys.periods p JOIN sys.columns c ON c.object_id = p.object_id AND c.column_id = p.start_column_id WHERE p.object_id = t.object_id)'
|
|
206
|
+
ELSE N'CAST(NULL AS sysname)' END + N' AS [periodStart],
|
|
207
|
+
' + CASE WHEN @temporal = 1
|
|
208
|
+
THEN N'(SELECT c.[name] FROM sys.periods p JOIN sys.columns c ON c.object_id = p.object_id AND c.column_id = p.end_column_id WHERE p.object_id = t.object_id)'
|
|
209
|
+
ELSE N'CAST(NULL AS sysname)' END + N' AS [periodEnd],
|
|
210
|
+
' + CASE WHEN COL_LENGTH('sys.tables', 'is_memory_optimized') IS NOT NULL THEN N't.is_memory_optimized' ELSE N'CAST(0 AS bit)' END + N' AS [memoryOptimised],
|
|
211
|
+
' + CASE WHEN COL_LENGTH('sys.tables', 'durability_desc') IS NOT NULL THEN N't.durability_desc' ELSE N'CAST(NULL AS nvarchar(60))' END + N' AS [durability],
|
|
212
|
+
' + CASE WHEN COL_LENGTH('sys.tables', 'ledger_type_desc') IS NOT NULL THEN N't.ledger_type_desc' ELSE N'CAST(NULL AS nvarchar(60))' END + N' AS [ledger],
|
|
213
|
+
t.is_tracked_by_cdc AS [cdc],
|
|
214
|
+
CASE WHEN ct.object_id IS NULL THEN 0 ELSE 1 END AS [changeTracking],
|
|
215
|
+
ps.[name] AS [scheme],
|
|
216
|
+
pf.[name] AS [function],
|
|
217
|
+
pc.[name] AS [partitionColumn],
|
|
218
|
+
(SELECT COUNT(*) FROM sys.partitions p WHERE p.object_id = t.object_id AND p.index_id IN (0, 1)) AS [partitions],
|
|
219
|
+
-- The compression of every partition, comma-separated, since partitions may differ.
|
|
220
|
+
STUFF((SELECT DISTINCT N'','' + p.data_compression_desc FROM sys.partitions p WHERE p.object_id = t.object_id AND p.index_id IN (0, 1) FOR XML PATH(N'''')), 1, 1, N'''') AS [compression]
|
|
221
|
+
FROM sys.tables t
|
|
222
|
+
JOIN sys.schemas s ON s.schema_id = t.schema_id
|
|
223
|
+
LEFT JOIN sys.indexes i ON i.object_id = t.object_id AND i.index_id IN (0, 1)
|
|
224
|
+
LEFT JOIN sys.partition_schemes ps ON ps.data_space_id = i.data_space_id
|
|
225
|
+
LEFT JOIN sys.partition_functions pf ON pf.function_id = ps.function_id
|
|
226
|
+
LEFT JOIN sys.index_columns ic ON ic.object_id = i.object_id AND ic.index_id = i.index_id AND ic.partition_ordinal = 1
|
|
227
|
+
LEFT JOIN sys.columns pc ON pc.object_id = ic.object_id AND pc.column_id = ic.column_id
|
|
228
|
+
LEFT JOIN sys.change_tracking_tables ct ON ct.object_id = t.object_id
|
|
229
|
+
WHERE t.is_ms_shipped = 0
|
|
230
|
+
ORDER BY s.[name], t.[name];';
|
|
231
|
+
EXEC sys.sp_executesql @features;
|
|
232
|
+
|
|
233
|
+
-- 10. Triggers on tables and views, one row per trigger and event. A trigger created WITH ENCRYPTION has no
|
|
234
|
+
-- definition to read.
|
|
235
|
+
SELECT s.[name] AS [schema],
|
|
236
|
+
o.[name] AS [table],
|
|
237
|
+
tr.[name] AS [name],
|
|
238
|
+
tr.is_instead_of_trigger AS [insteadOf],
|
|
239
|
+
tr.is_disabled AS [disabled],
|
|
240
|
+
te.type_desc AS [event],
|
|
241
|
+
m.[definition] AS [definition]
|
|
242
|
+
FROM sys.triggers tr
|
|
243
|
+
JOIN sys.objects o ON o.object_id = tr.parent_id AND o.[type] IN ('U', 'V') AND o.is_ms_shipped = 0
|
|
244
|
+
JOIN sys.schemas s ON s.schema_id = o.schema_id
|
|
245
|
+
JOIN sys.trigger_events te ON te.object_id = tr.object_id
|
|
246
|
+
LEFT JOIN sys.sql_modules m ON m.object_id = tr.object_id
|
|
247
|
+
WHERE tr.parent_class = 1
|
|
248
|
+
ORDER BY s.[name], o.[name], tr.[name], te.[type];
|
|
249
|
+
|
|
250
|
+
-- 11. Row-level security predicates on each table (SQL Server 2016 on).
|
|
251
|
+
IF OBJECT_ID('sys.security_predicates') IS NOT NULL
|
|
252
|
+
EXEC sys.sp_executesql N'
|
|
253
|
+
SELECT s.[name] AS [schema],
|
|
254
|
+
o.[name] AS [table],
|
|
255
|
+
ps.[name] AS [policySchema],
|
|
256
|
+
p.[name] AS [policy],
|
|
257
|
+
p.is_enabled AS [enabled],
|
|
258
|
+
sp.predicate_type_desc AS [kind],
|
|
259
|
+
sp.operation_desc AS [operation],
|
|
260
|
+
sp.predicate_definition AS [predicate]
|
|
261
|
+
FROM sys.security_predicates sp
|
|
262
|
+
JOIN sys.security_policies p ON p.object_id = sp.object_id
|
|
263
|
+
JOIN sys.schemas ps ON ps.schema_id = p.schema_id
|
|
264
|
+
JOIN sys.objects o ON o.object_id = sp.target_object_id
|
|
265
|
+
JOIN sys.schemas s ON s.schema_id = o.schema_id
|
|
266
|
+
ORDER BY s.[name], o.[name], p.[name], sp.security_predicate_id;';
|
|
267
|
+
ELSE
|
|
268
|
+
SELECT TOP (0) CAST(NULL AS sysname) AS [schema];
|
|
269
|
+
|
|
270
|
+
-- 12. Masked and Always Encrypted columns (SQL Server 2016 on).
|
|
271
|
+
IF COL_LENGTH('sys.columns', 'encryption_type') IS NOT NULL AND OBJECT_ID('sys.masked_columns') IS NOT NULL
|
|
272
|
+
EXEC sys.sp_executesql N'
|
|
273
|
+
SELECT s.[name] AS [schema],
|
|
274
|
+
o.[name] AS [table],
|
|
275
|
+
c.[name] AS [column],
|
|
276
|
+
mc.masking_function AS [masked],
|
|
277
|
+
c.encryption_type_desc AS [encryption],
|
|
278
|
+
k.[name] AS [key]
|
|
279
|
+
FROM sys.columns c
|
|
280
|
+
JOIN sys.objects o ON o.object_id = c.object_id AND o.[type] IN (''U'', ''V'') AND o.is_ms_shipped = 0
|
|
281
|
+
JOIN sys.schemas s ON s.schema_id = o.schema_id
|
|
282
|
+
LEFT JOIN sys.masked_columns mc ON mc.object_id = c.object_id AND mc.column_id = c.column_id AND mc.is_masked = 1
|
|
283
|
+
LEFT JOIN sys.column_encryption_keys k ON k.column_encryption_key_id = c.column_encryption_key_id
|
|
284
|
+
WHERE mc.object_id IS NOT NULL OR c.encryption_type IS NOT NULL;';
|
|
285
|
+
ELSE
|
|
286
|
+
SELECT TOP (0) CAST(NULL AS sysname) AS [schema];
|
|
287
|
+
|
|
288
|
+
-- 13. Sensitivity labels on columns (SQL Server 2019 on, and Azure SQL, which fills them in from data
|
|
289
|
+
-- discovery), only when the login may read them. The nested IF keeps the permission's name, unknown to
|
|
290
|
+
-- an older server, from being asked about there.
|
|
291
|
+
DECLARE @labelsRead bit = 0;
|
|
292
|
+
IF OBJECT_ID('sys.sensitivity_classifications') IS NOT NULL
|
|
293
|
+
IF HAS_PERMS_BY_NAME(DB_NAME(), 'DATABASE', 'VIEW ANY SENSITIVITY CLASSIFICATION') = 1 SET @labelsRead = 1;
|
|
294
|
+
IF @labelsRead = 1
|
|
295
|
+
BEGIN
|
|
296
|
+
DECLARE @labels nvarchar(max) = CAST(N'' AS nvarchar(max)) + N'
|
|
297
|
+
SELECT s.[name] AS [schema],
|
|
298
|
+
o.[name] AS [table],
|
|
299
|
+
c.[name] AS [column],
|
|
300
|
+
CAST(sc.label AS nvarchar(256)) AS [label],
|
|
301
|
+
CAST(sc.information_type AS nvarchar(256)) AS [informationType],
|
|
302
|
+
' + CASE WHEN COL_LENGTH('sys.sensitivity_classifications', 'rank_desc') IS NOT NULL THEN N'sc.rank_desc' ELSE N'CAST(NULL AS nvarchar(60))' END + N' AS [rank]
|
|
303
|
+
FROM sys.sensitivity_classifications sc
|
|
304
|
+
JOIN sys.objects o ON o.object_id = sc.major_id AND o.is_ms_shipped = 0
|
|
305
|
+
JOIN sys.schemas s ON s.schema_id = o.schema_id
|
|
306
|
+
JOIN sys.columns c ON c.object_id = sc.major_id AND c.column_id = sc.minor_id
|
|
307
|
+
WHERE sc.class = 1;';
|
|
308
|
+
EXEC sys.sp_executesql @labels;
|
|
309
|
+
END
|
|
310
|
+
ELSE
|
|
311
|
+
SELECT TOP (0) CAST(NULL AS sysname) AS [schema];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dbtour-source-mssql",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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.4.0"
|
|
40
40
|
}
|
|
41
41
|
}
|