dbtour 0.2.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/model.d.ts CHANGED
@@ -22,6 +22,17 @@ export interface Constraint {
22
22
  columns: string[];
23
23
  definition?: string;
24
24
  }
25
+ /**
26
+ * How big a table is, as the database keeps it about itself: counted from its catalogue, never by reading
27
+ * rows, so the count is the database's own estimate. Absent when the source could not read it.
28
+ */
29
+ export interface TableStats {
30
+ rows: number;
31
+ /** The pages the rows take, the heap or clustered index, in bytes. */
32
+ dataBytes: number;
33
+ /** The pages every other index takes, in bytes. */
34
+ indexBytes: number;
35
+ }
25
36
  export interface Table {
26
37
  name: string;
27
38
  kind: 'table' | 'view';
@@ -32,6 +43,7 @@ export interface Table {
32
43
  constraints: Constraint[];
33
44
  /** The SQL of a view. */
34
45
  definition?: string;
46
+ stats?: TableStats;
35
47
  }
36
48
  export interface Schema {
37
49
  name: string;
@@ -55,6 +67,8 @@ export interface Database {
55
67
  driver: string;
56
68
  version?: string;
57
69
  description?: string;
70
+ /** When the source read its tables' stats, as an ISO timestamp: they change, so they carry their moment. */
71
+ statsReadAt?: string;
58
72
  schemas: Schema[];
59
73
  relations: Relation[];
60
74
  }
package/dist/model.js CHANGED
@@ -113,6 +113,7 @@ function checkDatabase(check, d, p) {
113
113
  check.string(d.driver, `${p}.driver`);
114
114
  check.optionalString(d.version, `${p}.version`);
115
115
  check.optionalString(d.description, `${p}.description`);
116
+ check.optionalString(d.statsReadAt, `${p}.statsReadAt`);
116
117
  check.list(d.schemas, `${p}.schemas`, (s, sp) => {
117
118
  if (!check.object(s, sp))
118
119
  return;
@@ -141,6 +142,10 @@ function checkTable(check, t, p) {
141
142
  check.errors.push(`${p}.kind must be "table" or "view"`);
142
143
  check.optionalString(t.description, `${p}.description`);
143
144
  check.optionalString(t.definition, `${p}.definition`);
145
+ if (t.stats !== undefined && check.object(t.stats, `${p}.stats`)) {
146
+ for (const field of ['rows', 'dataBytes', 'indexBytes'])
147
+ check.count(t.stats[field], `${p}.stats.${field}`);
148
+ }
144
149
  if (t.primaryKey !== undefined)
145
150
  check.strings(t.primaryKey, `${p}.primaryKey`);
146
151
  check.list(t.columns, `${p}.columns`, (c, cp) => {
@@ -196,6 +201,10 @@ class Checker {
196
201
  if (value !== undefined && typeof value !== 'boolean')
197
202
  this.errors.push(`${path} must be true or false when given`);
198
203
  }
204
+ count(value, path) {
205
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < 0)
206
+ this.errors.push(`${path} must be a number of 0 or more`);
207
+ }
199
208
  strings(value, path) {
200
209
  if (!Array.isArray(value) || value.some((v) => typeof v !== 'string'))
201
210
  this.errors.push(`${path} must be a list of strings`);
@@ -1,8 +1,14 @@
1
- import { findTable, type Model } from '../model.ts';
1
+ import { findTable, type Model, type TableStats } from '../model.ts';
2
2
  export declare const wholeDiagramLimit = 40;
3
3
  export declare function renderMarkdown(model: Model): Record<string, string>;
4
4
  /** The product's name for a driver: mssql is SQL Server. */
5
5
  export declare function driverName(driver: string): string;
6
+ /** 1,204,377: thousands grouped the same way wherever it runs, so the docs do not change with the machine. */
7
+ export declare function formatCount(n: number): string;
8
+ /** 480 KB, 7.1 MB, 102 MB: binary units, as the database counts its pages, with one decimal below ten. */
9
+ export declare function formatBytes(bytes: number): string;
10
+ /** The sentence under a table's title: the count is the database's estimate, so it says about, and when. */
11
+ export declare function statsLine(stats: TableStats, readAt?: string): string;
6
12
  /** One table's page on its own: what the page hands to an agent. */
7
13
  export declare function renderTableMarkdown(model: Model, ref: string): string;
8
14
  export { findTable };
@@ -11,7 +11,7 @@ export function renderMarkdown(model) {
11
11
  for (const schema of database.schemas) {
12
12
  for (const table of schema.tables) {
13
13
  const ref = tableRef(schema.name, table.name);
14
- files[`tables/${ref}.md`] = renderTable(model, ref, table);
14
+ files[`tables/${ref}.md`] = renderTable(model, ref, table, database);
15
15
  }
16
16
  }
17
17
  }
@@ -27,11 +27,14 @@ function renderIndex(model) {
27
27
  if (database.description)
28
28
  parts.push(`${database.description}\n`);
29
29
  parts.push(`${driverLine(database)}\n`);
30
- parts.push('| Table | Kind | Columns | Description |\n|---|---|---|---|');
30
+ // Rows and size only when the source read them; a view, which keeps no rows of its own, leaves them blank.
31
+ const sized = database.schemas.some((s) => s.tables.some((t) => t.stats));
32
+ parts.push(sized ? '| Table | Kind | Columns | Rows | Size | Description |\n|---|---|---|---|---|---|' : '| Table | Kind | Columns | Description |\n|---|---|---|---|');
31
33
  for (const schema of database.schemas) {
32
34
  for (const table of schema.tables) {
33
35
  const ref = tableRef(schema.name, table.name);
34
- parts.push(`| [${ref}](tables/${ref}.md) | ${table.kind} | ${table.columns.length} | ${cell(table.description)} |`);
36
+ const size = !sized ? '' : table.stats ? ` ${formatCount(table.stats.rows)} | ${formatBytes(table.stats.dataBytes + table.stats.indexBytes)} |` : ' | |';
37
+ parts.push(`| [${ref}](tables/${ref}.md) | ${table.kind} | ${table.columns.length} |${size} ${cell(table.description)} |`);
35
38
  }
36
39
  }
37
40
  parts.push('');
@@ -62,17 +65,40 @@ function driverLine(database) {
62
65
  const name = driverName(database.driver);
63
66
  return database.version ? `${name} ${database.version}.` : `${name}.`;
64
67
  }
68
+ /** 1,204,377: thousands grouped the same way wherever it runs, so the docs do not change with the machine. */
69
+ export function formatCount(n) {
70
+ return String(Math.round(n)).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
71
+ }
72
+ const byteUnits = ['B', 'KB', 'MB', 'GB', 'TB'];
73
+ /** 480 KB, 7.1 MB, 102 MB: binary units, as the database counts its pages, with one decimal below ten. */
74
+ export function formatBytes(bytes) {
75
+ let value = bytes;
76
+ let unit = 0;
77
+ while (value >= 1024 && unit < byteUnits.length - 1) {
78
+ value /= 1024;
79
+ unit++;
80
+ }
81
+ const text = unit === 0 || value >= 10 ? String(Math.round(value)) : value.toFixed(1).replace(/\.0$/, '');
82
+ return `${text} ${byteUnits[unit]}`;
83
+ }
84
+ /** The sentence under a table's title: the count is the database's estimate, so it says about, and when. */
85
+ export function statsLine(stats, readAt) {
86
+ const when = readAt ? `, as the database counted them on ${readAt.slice(0, 10)}` : '';
87
+ return `About ${formatCount(stats.rows)} rows · ${formatBytes(stats.dataBytes)} of data · ${formatBytes(stats.indexBytes)} of indexes${when}.`;
88
+ }
65
89
  /** One table's page on its own: what the page hands to an agent. */
66
90
  export function renderTableMarkdown(model, ref) {
67
91
  const found = findTable(model, ref);
68
92
  if (!found)
69
93
  throw new Error(`${ref} is not a table in the model`);
70
- return renderTable(model, ref, found.table);
94
+ return renderTable(model, ref, found.table, found.database);
71
95
  }
72
- function renderTable(model, ref, table) {
96
+ function renderTable(model, ref, table, database) {
73
97
  const parts = [`# ${ref}\n`];
74
98
  if (table.description)
75
99
  parts.push(`${table.description}\n`);
100
+ if (table.stats)
101
+ parts.push(`${statsLine(table.stats, database.statsReadAt)}\n`);
76
102
  const relations = relationsOf(model, ref);
77
103
  parts.push('| Column | Type | Nullable | Default | Key | Description |\n|---|---|---|---|---|---|');
78
104
  for (const column of table.columns) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dbtour",
3
- "version": "0.2.4",
3
+ "version": "0.3.0",
4
4
  "description": "Tour your database and its API: searchable docs, relations and diagrams, generated from the schema, kept in the repo.",
5
5
  "type": "module",
6
6
  "license": "MIT",