dbtour-source-mssql 0.3.0 → 0.5.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 CHANGED
@@ -1,11 +1,15 @@
1
1
  import type { Database } 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
+ /** 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';
4
6
  export interface InfoRow {
5
7
  name: string;
6
8
  version: string | null;
7
9
  description: string | null;
8
10
  readAt?: string | null;
11
+ collation?: string | null;
12
+ labels?: Labels | null;
9
13
  }
10
14
  export interface TableRow {
11
15
  schema: string;
@@ -16,6 +20,8 @@ export interface TableRow {
16
20
  rows?: Count | null;
17
21
  dataBytes?: Count | null;
18
22
  indexBytes?: Count | null;
23
+ created?: string | null;
24
+ altered?: string | null;
19
25
  }
20
26
  export interface ColumnRow {
21
27
  schema: string;
@@ -31,6 +37,10 @@ export interface ColumnRow {
31
37
  default: string | null;
32
38
  computed: string | null;
33
39
  description: string | null;
40
+ collation?: string | null;
41
+ sparse?: boolean | null;
42
+ identitySeed?: string | null;
43
+ identityIncrement?: string | null;
34
44
  }
35
45
  export interface KeyRow {
36
46
  schema: string;
@@ -51,6 +61,8 @@ export interface ForeignKeyRow {
51
61
  ordinal: number;
52
62
  onDelete: string;
53
63
  onUpdate: string;
64
+ disabled?: boolean | null;
65
+ untrusted?: boolean | null;
54
66
  }
55
67
  export interface IndexRow {
56
68
  schema: string;
@@ -61,6 +73,9 @@ export interface IndexRow {
61
73
  column: string;
62
74
  ordinal: number;
63
75
  included: boolean;
76
+ type?: string | null;
77
+ filter?: string | null;
78
+ disabled?: boolean | null;
64
79
  }
65
80
  export interface CheckRow {
66
81
  schema: string;
@@ -68,6 +83,73 @@ export interface CheckRow {
68
83
  name: string;
69
84
  definition: string;
70
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;
71
153
  }
72
154
  export interface Recordsets {
73
155
  info: InfoRow[];
@@ -77,6 +159,12 @@ export interface Recordsets {
77
159
  foreignKeys: ForeignKeyRow[];
78
160
  indexes: IndexRow[];
79
161
  checks: CheckRow[];
162
+ properties: PropertyRow[];
163
+ features: FeatureRow[];
164
+ triggers: TriggerRow[];
165
+ security: SecurityRow[];
166
+ columnSecurity: ColumnSecurityRow[];
167
+ sensitivity: SensitivityRow[];
80
168
  }
81
169
  export interface MapOptions {
82
170
  /** The name the docs call the database; the server's own name when not given. */
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 !== 7)
4
- throw new Error(`query.sql returns 7 result sets, got ${sets.length}: the query and map.ts disagree`);
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];
@@ -26,6 +26,10 @@ export function mapRecordsets(sets, options = {}) {
26
26
  // table: an indexed view has rows in sys.partitions too, but they are the view's results, not stored data.
27
27
  if (row.kind === 'table' && row.rows !== null && row.rows !== undefined)
28
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;
29
33
  schema.tables.push(table);
30
34
  tables.set(`${row.schema}.${row.name}`, table);
31
35
  }
@@ -40,8 +44,17 @@ export function mapRecordsets(sets, options = {}) {
40
44
  column.computed = row.computed;
41
45
  if (row.identity)
42
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
43
51
  if (row.description)
44
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;
45
58
  table.columns.push(column);
46
59
  }
47
60
  for (const [key, rows] of groupBy(sets.keys, (r) => `${r.schema}.${r.table}|${r.name}`)) {
@@ -66,8 +79,13 @@ export function mapRecordsets(sets, options = {}) {
66
79
  relation.onDelete = first.onDelete;
67
80
  if (first.onUpdate && first.onUpdate !== 'NO_ACTION')
68
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;
69
87
  relations.push(relation);
70
- tables.get(from).constraints.push({ name: first.name, kind: 'foreign', columns: relation.from.columns, definition: `${to} (${relation.to.columns.join(', ')})` });
88
+ tables.get(from).constraints.push(constraint);
71
89
  }
72
90
  for (const [key, rows] of groupBy(sets.indexes, (r) => `${r.schema}.${r.table}|${r.name}`)) {
73
91
  const table = tables.get(key.split('|')[0]);
@@ -81,6 +99,12 @@ export function mapRecordsets(sets, options = {}) {
81
99
  const included = rows.filter((r) => r.included).map((r) => r.column);
82
100
  if (included.length > 0)
83
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;
84
108
  table.indexes.push(index);
85
109
  }
86
110
  for (const row of sets.checks) {
@@ -88,17 +112,141 @@ export function mapRecordsets(sets, options = {}) {
88
112
  if (!table)
89
113
  continue;
90
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;
91
119
  table.constraints.push(constraint);
92
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
+ }
93
168
  const database = { name: options.name ?? info.name, driver: 'mssql', schemas, relations };
94
169
  if (info.version)
95
170
  database.version = info.version;
96
171
  if (info.description)
97
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'];
98
178
  if (info.readAt && [...tables.values()].some((t) => t.stats))
99
179
  database.statsReadAt = info.readAt;
100
180
  return database;
101
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
+ }
102
250
  /** The type as SQL Server would spell it in a CREATE TABLE: nvarchar(320), varchar(max), decimal(18,2), datetime2(7). */
103
251
  export function formatType(row) {
104
252
  const t = row.type.toLowerCase();
package/dist/query.sql CHANGED
@@ -1,14 +1,22 @@
1
- -- Everything dbtour needs from SQL Server, in one batch of seven result sets, read in this order by map.ts.
1
+ -- Everything dbtour needs from SQL Server, in one batch of thirteen result sets, read in this order by map.ts.
2
2
  -- Reads the catalogue views only: a login with VIEW DEFINITION on the database is enough. That is why the
3
3
  -- sizes come from sys.partitions and sys.allocation_units, catalogue views, rather than
4
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.
5
9
  SET NOCOUNT ON;
6
10
 
7
11
  -- 1. The database
8
12
  SELECT DB_NAME() AS [name],
9
13
  CAST(SERVERPROPERTY('ProductVersion') AS nvarchar(50)) AS [version],
10
14
  (SELECT TOP (1) CAST([value] AS nvarchar(max)) FROM sys.extended_properties WHERE class = 0 AND [name] = 'MS_Description') AS [description],
11
- CONVERT(nvarchar(33), SYSUTCDATETIME(), 127) + 'Z' AS [readAt];
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];
12
20
 
13
21
  -- 2. Tables and views, with each table's rows and bytes as the catalogue counts them (no row is read).
14
22
  -- Rows are the heap's or the clustered index's (index_id 0 or 1); data bytes are those pages, index bytes
@@ -18,6 +26,9 @@ SELECT s.[name] AS [schema],
18
26
  CASE o.[type] WHEN 'V' THEN 'view' ELSE 'table' END AS [kind],
19
27
  CAST(ep.[value] AS nvarchar(max)) AS [description],
20
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],
21
32
  r.[rows] AS [rows],
22
33
  b.dataBytes AS [dataBytes],
23
34
  b.indexBytes AS [indexBytes]
@@ -61,7 +72,11 @@ SELECT s.[name] AS [schema],
61
72
  c.is_identity AS [identity],
62
73
  d.[definition] AS [default],
63
74
  cc.[definition] AS [computed],
64
- 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]
65
80
  FROM sys.columns c
66
81
  JOIN sys.objects o ON o.object_id = c.object_id AND o.[type] IN ('U', 'V') AND o.is_ms_shipped = 0
67
82
  JOIN sys.schemas s ON s.schema_id = o.schema_id
@@ -69,6 +84,7 @@ JOIN sys.types t ON t.user_type_id = c.user_type_id
69
84
  LEFT JOIN sys.default_constraints d ON d.object_id = c.default_object_id
70
85
  LEFT JOIN sys.computed_columns cc ON cc.object_id = c.object_id AND cc.column_id = c.column_id
71
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
72
88
  ORDER BY s.[name], o.[name], c.column_id;
73
89
 
74
90
  -- 4. Primary keys and unique constraints, one row per column
@@ -95,7 +111,9 @@ SELECT fk.[name] AS [name],
95
111
  rc.[name] AS [toColumn],
96
112
  fkc.constraint_column_id AS [ordinal],
97
113
  fk.delete_referential_action_desc AS [onDelete],
98
- 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]
99
117
  FROM sys.foreign_keys fk
100
118
  JOIN sys.foreign_key_columns fkc ON fkc.constraint_object_id = fk.object_id
101
119
  JOIN sys.objects po ON po.object_id = fk.parent_object_id
@@ -114,7 +132,10 @@ SELECT s.[name] AS [schema],
114
132
  i.is_primary_key AS [primary],
115
133
  c.[name] AS [column],
116
134
  ic.key_ordinal AS [ordinal],
117
- 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]
118
139
  FROM sys.indexes i
119
140
  JOIN sys.objects o ON o.object_id = i.object_id AND o.[type] IN ('U', 'V') AND o.is_ms_shipped = 0
120
141
  JOIN sys.schemas s ON s.schema_id = o.schema_id
@@ -128,9 +149,163 @@ SELECT s.[name] AS [schema],
128
149
  o.[name] AS [table],
129
150
  ck.[name] AS [name],
130
151
  ck.[definition] AS [definition],
131
- c.[name] AS [column]
152
+ c.[name] AS [column],
153
+ ck.is_disabled AS [disabled],
154
+ ck.is_not_trusted AS [untrusted]
132
155
  FROM sys.check_constraints ck
133
156
  JOIN sys.objects o ON o.object_id = ck.parent_object_id AND o.is_ms_shipped = 0
134
157
  JOIN sys.schemas s ON s.schema_id = o.schema_id
135
158
  LEFT JOIN sys.columns c ON c.object_id = ck.parent_object_id AND c.column_id = ck.parent_column_id
136
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.0",
3
+ "version": "0.5.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.3.0"
39
+ "dbtour": "0.5.0"
40
40
  }
41
41
  }