dbtour-source-mssql 0.0.0-stage → 0.2.4

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.
@@ -0,0 +1,4 @@
1
+ import type { Database, SourceConfig } from 'dbtour';
2
+ export { mapRecordsets, toRecordsets, formatType } from './map.ts';
3
+ export declare const query: string;
4
+ export declare function extract(config: SourceConfig): Promise<Database>;
package/dist/index.js ADDED
@@ -0,0 +1,23 @@
1
+ // The SQL Server source. `connection` is anything the mssql package accepts: an ADO string
2
+ // (Server=host,1433;Database=Shop;User Id=reader;Password=…;TrustServerCertificate=True) or an
3
+ // mssql:// URL. It is read from the environment variable dbtour.yaml names, never from the file.
4
+ import { readFileSync } from 'node:fs';
5
+ import sql from 'mssql';
6
+ import { mapRecordsets, toRecordsets } from "./map.js";
7
+ export { mapRecordsets, toRecordsets, formatType } from "./map.js";
8
+ export const query = readFileSync(new URL('./query.sql', import.meta.url), 'utf8');
9
+ export async function extract(config) {
10
+ if (!config.connection) {
11
+ throw new Error(`Source ${config.name} has no connection: add "connection: \${DBTOUR_MSSQL}" to it in dbtour.yaml and set that variable`);
12
+ }
13
+ const pool = new sql.ConnectionPool(config.connection);
14
+ await pool.connect();
15
+ try {
16
+ const result = await pool.request().query(query);
17
+ const exclude = Array.isArray(config.exclude) ? config.exclude.map(String) : undefined;
18
+ return mapRecordsets(toRecordsets(result.recordsets), { name: config.name, exclude });
19
+ }
20
+ finally {
21
+ await pool.close();
22
+ }
23
+ }
package/dist/map.d.ts ADDED
@@ -0,0 +1,85 @@
1
+ import type { Database } from 'dbtour';
2
+ export interface InfoRow {
3
+ name: string;
4
+ version: string | null;
5
+ description: string | null;
6
+ }
7
+ export interface TableRow {
8
+ schema: string;
9
+ name: string;
10
+ kind: 'table' | 'view';
11
+ description: string | null;
12
+ definition: string | null;
13
+ }
14
+ export interface ColumnRow {
15
+ schema: string;
16
+ table: string;
17
+ name: string;
18
+ position: number;
19
+ type: string;
20
+ maxLength: number;
21
+ precision: number;
22
+ scale: number;
23
+ nullable: boolean;
24
+ identity: boolean;
25
+ default: string | null;
26
+ computed: string | null;
27
+ description: string | null;
28
+ }
29
+ export interface KeyRow {
30
+ schema: string;
31
+ table: string;
32
+ name: string;
33
+ kind: 'PK' | 'UQ';
34
+ column: string;
35
+ ordinal: number;
36
+ }
37
+ export interface ForeignKeyRow {
38
+ name: string;
39
+ schema: string;
40
+ table: string;
41
+ column: string;
42
+ toSchema: string;
43
+ toTable: string;
44
+ toColumn: string;
45
+ ordinal: number;
46
+ onDelete: string;
47
+ onUpdate: string;
48
+ }
49
+ export interface IndexRow {
50
+ schema: string;
51
+ table: string;
52
+ name: string;
53
+ unique: boolean;
54
+ primary: boolean;
55
+ column: string;
56
+ ordinal: number;
57
+ included: boolean;
58
+ }
59
+ export interface CheckRow {
60
+ schema: string;
61
+ table: string;
62
+ name: string;
63
+ definition: string;
64
+ column: string | null;
65
+ }
66
+ export interface Recordsets {
67
+ info: InfoRow[];
68
+ tables: TableRow[];
69
+ columns: ColumnRow[];
70
+ keys: KeyRow[];
71
+ foreignKeys: ForeignKeyRow[];
72
+ indexes: IndexRow[];
73
+ checks: CheckRow[];
74
+ }
75
+ export interface MapOptions {
76
+ /** The name the docs call the database; the server's own name when not given. */
77
+ name?: string;
78
+ /** Tables to leave out, as schema.table with * for any run of characters: `dbo.sys*`, `*.webpages_*`. */
79
+ exclude?: string[];
80
+ }
81
+ /** The result sets in the order query.sql returns them. */
82
+ export declare function toRecordsets(sets: unknown[][]): Recordsets;
83
+ export declare function mapRecordsets(sets: Recordsets, options?: MapOptions): Database;
84
+ /** The type as SQL Server would spell it in a CREATE TABLE: nvarchar(320), varchar(max), decimal(18,2), datetime2(7). */
85
+ export declare function formatType(row: Pick<ColumnRow, 'type' | 'maxLength' | 'precision' | 'scale'>): string;
package/dist/map.js ADDED
@@ -0,0 +1,134 @@
1
+ /** The result sets in the order query.sql returns them. */
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 };
7
+ }
8
+ export function mapRecordsets(sets, options = {}) {
9
+ const info = sets.info[0];
10
+ const excluded = (options.exclude ?? []).map(pattern);
11
+ const keep = (schema, table) => !excluded.some((p) => p.test(`${schema}.${table}`));
12
+ const tables = new Map();
13
+ const schemas = [];
14
+ for (const row of sets.tables) {
15
+ if (!keep(row.schema, row.name))
16
+ continue;
17
+ let schema = schemas.find((s) => s.name === row.schema);
18
+ if (!schema)
19
+ schemas.push((schema = { name: row.schema, tables: [] }));
20
+ const table = { name: row.name, kind: row.kind, columns: [], indexes: [], constraints: [] };
21
+ if (row.description)
22
+ table.description = row.description;
23
+ if (row.kind === 'view' && row.definition)
24
+ table.definition = row.definition;
25
+ schema.tables.push(table);
26
+ tables.set(`${row.schema}.${row.name}`, table);
27
+ }
28
+ for (const row of sets.columns) {
29
+ const table = tables.get(`${row.schema}.${row.table}`);
30
+ if (!table)
31
+ continue;
32
+ const column = { name: row.name, type: formatType(row), nullable: Boolean(row.nullable) };
33
+ if (row.default !== null)
34
+ column.default = row.default;
35
+ if (row.computed !== null)
36
+ column.computed = row.computed;
37
+ if (row.identity)
38
+ column.identity = true;
39
+ if (row.description)
40
+ column.description = row.description;
41
+ table.columns.push(column);
42
+ }
43
+ for (const [key, rows] of groupBy(sets.keys, (r) => `${r.schema}.${r.table}|${r.name}`)) {
44
+ const table = tables.get(key.split('|')[0]);
45
+ if (!table)
46
+ continue;
47
+ const columns = rows.sort((a, b) => a.ordinal - b.ordinal).map((r) => r.column);
48
+ if (rows[0].kind === 'PK')
49
+ table.primaryKey = columns;
50
+ table.constraints.push({ name: rows[0].name, kind: rows[0].kind === 'PK' ? 'primary' : 'unique', columns });
51
+ }
52
+ const relations = [];
53
+ for (const [, rows] of groupBy(sets.foreignKeys, (r) => r.name)) {
54
+ const ordered = rows.sort((a, b) => a.ordinal - b.ordinal);
55
+ const first = ordered[0];
56
+ const from = `${first.schema}.${first.table}`;
57
+ const to = `${first.toSchema}.${first.toTable}`;
58
+ if (!tables.has(from) || !tables.has(to))
59
+ continue;
60
+ const relation = { name: first.name, from: { table: from, columns: ordered.map((r) => r.column) }, to: { table: to, columns: ordered.map((r) => r.toColumn) } };
61
+ if (first.onDelete && first.onDelete !== 'NO_ACTION')
62
+ relation.onDelete = first.onDelete;
63
+ if (first.onUpdate && first.onUpdate !== 'NO_ACTION')
64
+ relation.onUpdate = first.onUpdate;
65
+ relations.push(relation);
66
+ tables.get(from).constraints.push({ name: first.name, kind: 'foreign', columns: relation.from.columns, definition: `${to} (${relation.to.columns.join(', ')})` });
67
+ }
68
+ for (const [key, rows] of groupBy(sets.indexes, (r) => `${r.schema}.${r.table}|${r.name}`)) {
69
+ const table = tables.get(key.split('|')[0]);
70
+ if (!table || rows[0].primary)
71
+ continue;
72
+ const index = {
73
+ name: rows[0].name,
74
+ columns: rows.filter((r) => !r.included).sort((a, b) => a.ordinal - b.ordinal).map((r) => r.column),
75
+ unique: Boolean(rows[0].unique),
76
+ };
77
+ const included = rows.filter((r) => r.included).map((r) => r.column);
78
+ if (included.length > 0)
79
+ index.included = included;
80
+ table.indexes.push(index);
81
+ }
82
+ for (const row of sets.checks) {
83
+ const table = tables.get(`${row.schema}.${row.table}`);
84
+ if (!table)
85
+ continue;
86
+ const constraint = { name: row.name, kind: 'check', columns: row.column ? [row.column] : [], definition: row.definition };
87
+ table.constraints.push(constraint);
88
+ }
89
+ const database = { name: options.name ?? info.name, driver: 'mssql', schemas, relations };
90
+ if (info.version)
91
+ database.version = info.version;
92
+ if (info.description)
93
+ database.description = info.description;
94
+ return database;
95
+ }
96
+ /** The type as SQL Server would spell it in a CREATE TABLE: nvarchar(320), varchar(max), decimal(18,2), datetime2(7). */
97
+ export function formatType(row) {
98
+ const t = row.type.toLowerCase();
99
+ switch (t) {
100
+ case 'char':
101
+ case 'varchar':
102
+ case 'binary':
103
+ case 'varbinary':
104
+ return `${t}(${row.maxLength === -1 ? 'max' : row.maxLength})`;
105
+ case 'nchar':
106
+ case 'nvarchar':
107
+ // max_length counts bytes; these types hold two per character.
108
+ return `${t}(${row.maxLength === -1 ? 'max' : row.maxLength / 2})`;
109
+ case 'decimal':
110
+ case 'numeric':
111
+ return `${t}(${row.precision},${row.scale})`;
112
+ case 'datetime2':
113
+ case 'datetimeoffset':
114
+ case 'time':
115
+ return `${t}(${row.scale})`;
116
+ default:
117
+ return t;
118
+ }
119
+ }
120
+ function groupBy(rows, key) {
121
+ const groups = new Map();
122
+ for (const row of rows) {
123
+ const k = key(row);
124
+ const group = groups.get(k);
125
+ if (group)
126
+ group.push(row);
127
+ else
128
+ groups.set(k, [row]);
129
+ }
130
+ return groups;
131
+ }
132
+ function pattern(text) {
133
+ return new RegExp('^' + text.split('*').map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('.*') + '$', 'i');
134
+ }
package/dist/query.sql ADDED
@@ -0,0 +1,108 @@
1
+ -- Everything dbtour needs from SQL Server, in one batch of seven 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.
3
+ SET NOCOUNT ON;
4
+
5
+ -- 1. The database
6
+ SELECT DB_NAME() AS [name],
7
+ 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];
9
+
10
+ -- 2. Tables and views
11
+ SELECT s.[name] AS [schema],
12
+ o.[name] AS [name],
13
+ CASE o.[type] WHEN 'V' THEN 'view' ELSE 'table' END AS [kind],
14
+ CAST(ep.[value] AS nvarchar(max)) AS [description],
15
+ m.[definition] AS [definition]
16
+ FROM sys.objects o
17
+ JOIN sys.schemas s ON s.schema_id = o.schema_id
18
+ 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
+ LEFT JOIN sys.sql_modules m ON m.object_id = o.object_id
20
+ WHERE o.[type] IN ('U', 'V') AND o.is_ms_shipped = 0
21
+ ORDER BY s.[name], o.[name];
22
+
23
+ -- 3. Columns
24
+ SELECT s.[name] AS [schema],
25
+ o.[name] AS [table],
26
+ c.[name] AS [name],
27
+ c.column_id AS [position],
28
+ t.[name] AS [type],
29
+ c.max_length AS [maxLength],
30
+ c.[precision] AS [precision],
31
+ c.scale AS [scale],
32
+ c.is_nullable AS [nullable],
33
+ c.is_identity AS [identity],
34
+ d.[definition] AS [default],
35
+ cc.[definition] AS [computed],
36
+ CAST(ep.[value] AS nvarchar(max)) AS [description]
37
+ FROM sys.columns c
38
+ JOIN sys.objects o ON o.object_id = c.object_id AND o.[type] IN ('U', 'V') AND o.is_ms_shipped = 0
39
+ JOIN sys.schemas s ON s.schema_id = o.schema_id
40
+ JOIN sys.types t ON t.user_type_id = c.user_type_id
41
+ LEFT JOIN sys.default_constraints d ON d.object_id = c.default_object_id
42
+ LEFT JOIN sys.computed_columns cc ON cc.object_id = c.object_id AND cc.column_id = c.column_id
43
+ 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'
44
+ ORDER BY s.[name], o.[name], c.column_id;
45
+
46
+ -- 4. Primary keys and unique constraints, one row per column
47
+ SELECT s.[name] AS [schema],
48
+ o.[name] AS [table],
49
+ kc.[name] AS [name],
50
+ kc.[type] AS [kind],
51
+ c.[name] AS [column],
52
+ ic.key_ordinal AS [ordinal]
53
+ FROM sys.key_constraints kc
54
+ JOIN sys.objects o ON o.object_id = kc.parent_object_id AND o.is_ms_shipped = 0
55
+ JOIN sys.schemas s ON s.schema_id = o.schema_id
56
+ JOIN sys.index_columns ic ON ic.object_id = kc.parent_object_id AND ic.index_id = kc.unique_index_id AND ic.key_ordinal > 0
57
+ JOIN sys.columns c ON c.object_id = ic.object_id AND c.column_id = ic.column_id
58
+ ORDER BY s.[name], o.[name], kc.[name], ic.key_ordinal;
59
+
60
+ -- 5. Foreign keys, one row per column pair
61
+ SELECT fk.[name] AS [name],
62
+ ps.[name] AS [schema],
63
+ po.[name] AS [table],
64
+ pc.[name] AS [column],
65
+ rs.[name] AS [toSchema],
66
+ ro.[name] AS [toTable],
67
+ rc.[name] AS [toColumn],
68
+ fkc.constraint_column_id AS [ordinal],
69
+ fk.delete_referential_action_desc AS [onDelete],
70
+ fk.update_referential_action_desc AS [onUpdate]
71
+ FROM sys.foreign_keys fk
72
+ JOIN sys.foreign_key_columns fkc ON fkc.constraint_object_id = fk.object_id
73
+ JOIN sys.objects po ON po.object_id = fk.parent_object_id
74
+ JOIN sys.schemas ps ON ps.schema_id = po.schema_id
75
+ JOIN sys.columns pc ON pc.object_id = fkc.parent_object_id AND pc.column_id = fkc.parent_column_id
76
+ JOIN sys.objects ro ON ro.object_id = fk.referenced_object_id
77
+ JOIN sys.schemas rs ON rs.schema_id = ro.schema_id
78
+ JOIN sys.columns rc ON rc.object_id = fkc.referenced_object_id AND rc.column_id = fkc.referenced_column_id
79
+ ORDER BY fk.[name], fkc.constraint_column_id;
80
+
81
+ -- 6. Indexes, one row per column, key columns then included ones
82
+ SELECT s.[name] AS [schema],
83
+ o.[name] AS [table],
84
+ i.[name] AS [name],
85
+ i.is_unique AS [unique],
86
+ i.is_primary_key AS [primary],
87
+ c.[name] AS [column],
88
+ ic.key_ordinal AS [ordinal],
89
+ ic.is_included_column AS [included]
90
+ FROM sys.indexes i
91
+ JOIN sys.objects o ON o.object_id = i.object_id AND o.[type] IN ('U', 'V') AND o.is_ms_shipped = 0
92
+ JOIN sys.schemas s ON s.schema_id = o.schema_id
93
+ JOIN sys.index_columns ic ON ic.object_id = i.object_id AND ic.index_id = i.index_id
94
+ JOIN sys.columns c ON c.object_id = ic.object_id AND c.column_id = ic.column_id
95
+ WHERE i.[name] IS NOT NULL AND i.[type] > 0
96
+ ORDER BY s.[name], o.[name], i.[name], ic.is_included_column, ic.key_ordinal, ic.index_column_id;
97
+
98
+ -- 7. Check constraints
99
+ SELECT s.[name] AS [schema],
100
+ o.[name] AS [table],
101
+ ck.[name] AS [name],
102
+ ck.[definition] AS [definition],
103
+ c.[name] AS [column]
104
+ FROM sys.check_constraints ck
105
+ JOIN sys.objects o ON o.object_id = ck.parent_object_id AND o.is_ms_shipped = 0
106
+ JOIN sys.schemas s ON s.schema_id = o.schema_id
107
+ LEFT JOIN sys.columns c ON c.object_id = ck.parent_object_id AND c.column_id = ck.parent_column_id
108
+ ORDER BY s.[name], o.[name], ck.[name];
package/package.json CHANGED
@@ -1,6 +1,41 @@
1
1
  {
2
2
  "name": "dbtour-source-mssql",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.4",
4
+ "description": "SQL Server source for dbtour: tables, views, columns, keys, relations, indexes, descriptions.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Victor Saly",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/victorsaly/dbtour.git",
11
+ "directory": "packages/source-mssql"
12
+ },
13
+ "engines": {
14
+ "node": ">=22.18"
15
+ },
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "default": "./dist/index.js"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "scripts": {
26
+ "prepack": "node ../../scripts/build.mjs"
27
+ },
28
+ "keywords": [
29
+ "dbtour",
30
+ "mssql",
31
+ "sql-server",
32
+ "schema"
33
+ ],
34
+ "dependencies": {
35
+ "mssql": "^12.7.13"
36
+ },
37
+ "devDependencies": {
38
+ "@types/mssql": "^9.1.8",
39
+ "dbtour": "0.2.4"
40
+ }
41
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.