dsh-plugin-dbgraph-catalog 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.d.ts ADDED
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Catalog dialect plugin for `ctx.dbgraph`: registers one {@link DatabaseDialect} per database
3
+ * engine so the service can reach a server, snapshot its structure, and execute the single read a
4
+ * `find` call produces.
5
+ *
6
+ * **A table of dialects, not a switch.** {@link DIALECT_FACTORIES} is the whole extensibility
7
+ * story: adding PostgreSQL, MongoDB or Redis later is one entry that loads its driver and returns a
8
+ * `ManagedDialect`; `apply` needs no change, and the seam's "one dialect per engine" rule is
9
+ * enforced by `ctx.dbgraph.registerDialect` itself.
10
+ *
11
+ * **An unavailable driver disables one dialect, never the package.** SQLite lives in Node's builtins
12
+ * and MySQL's driver is an optional dependency, so either can be absent on a given installation. A
13
+ * failed load logs one line naming the engine and continues; a catalog package that refused to load
14
+ * because an unrelated driver was missing would take the working dialect down with it.
15
+ *
16
+ * **Disposal is ordered.** The plugin's connection caches are closed after the dialect
17
+ * registrations are withdrawn (effects dispose last-in-first-out), so no call can be routed to a
18
+ * dialect whose handles are already shut.
19
+ * @module dsh-plugin-dbgraph-catalog
20
+ */
21
+ import type { Context } from '@deepseek-ai/cordis';
22
+ import type { Engine } from 'dsh-plugin-dbgraph-service';
23
+ import type { ManagedDialect } from './support.ts';
24
+ export { createMysqlDialect, loadMysqlDriver, MYSQL_DRIVER_PACKAGE, mysqlIdentifier } from './mysql.ts';
25
+ export type { MysqlModule } from './mysql.ts';
26
+ export { createSqliteDialect, isStringishSqliteType, sqliteIdentifier } from './sqlite.ts';
27
+ export type { SqliteModule } from './sqlite.ts';
28
+ export { HandleCache } from './pool.ts';
29
+ export { DBGRAPH_QUERY_FAILED_CODE, DBGRAPH_READ_ONLY_CODE, REDACTED_VALUE, assertReadOnlyStatement, clampRowLimit, describeWriteChannel, firstStatementKeyword, isReadStatement, messageOf, normalizeRows, renderBinary, throwIfAborted, toJsonSafe, } from './support.ts';
30
+ export type { ManagedDialect } from './support.ts';
31
+ /** Cordis plugin name for loader diagnostics. */
32
+ export declare const name = "dbgraph-catalog";
33
+ /** Services required by this plugin. */
34
+ export declare const inject: string[];
35
+ /** One engine's loader: resolves to a dialect, or rejects when its driver is unavailable. */
36
+ export interface DialectFactory {
37
+ /** The engine the loaded dialect serves. */
38
+ readonly engine: Engine;
39
+ /** Load the driver and build the dialect. */
40
+ readonly load: () => Promise<ManagedDialect>;
41
+ }
42
+ /** Why one engine's dialect could not be loaded. */
43
+ export interface DialectLoadFailure {
44
+ /** The engine whose driver was unavailable or whose dialect failed to construct. */
45
+ readonly engine: Engine;
46
+ /** Why it could not load, as the loader reported it. */
47
+ readonly error: string;
48
+ }
49
+ /** The outcome of trying every configured dialect. */
50
+ export interface DialectLoadResult {
51
+ /** Dialects that loaded and should be registered. */
52
+ readonly dialects: readonly ManagedDialect[];
53
+ /** Engines that were skipped, one entry each. */
54
+ readonly failures: readonly DialectLoadFailure[];
55
+ }
56
+ /**
57
+ * Every dialect this package can provide, in registration order.
58
+ *
59
+ * The loaders are async because MySQL's driver is imported on demand; SQLite's builtin is imported
60
+ * the same way so an older Node simply reports itself unavailable instead of crashing the module.
61
+ */
62
+ export declare const DIALECT_FACTORIES: readonly DialectFactory[];
63
+ /**
64
+ * Load as many dialects as the installation supports.
65
+ *
66
+ * A failed loader is collected rather than thrown: one engine's missing optional driver must never
67
+ * prevent the other engines from registering. The split from {@link apply} exists so the
68
+ * availability decision can be tested (and reused by a deployment that mounts dialects itself)
69
+ * without constructing a Cordis context.
70
+ * @param factories - factories to try; defaults to {@link DIALECT_FACTORIES}.
71
+ * @returns the dialects that loaded, plus one failure per skipped engine.
72
+ */
73
+ export declare function loadDialects(factories?: readonly DialectFactory[]): Promise<DialectLoadResult>;
74
+ /**
75
+ * Register every dialect whose driver is available.
76
+ * @param ctx - the plugin context (must inject `dbgraph`).
77
+ * @returns a promise that settles after registration (and after every failed load was reported).
78
+ */
79
+ export declare function apply(ctx: Context): Promise<void>;
80
+ //# sourceMappingURL=index.d.ts.map
package/lib/index.js ADDED
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Catalog dialect plugin for `ctx.dbgraph`: registers one {@link DatabaseDialect} per database
3
+ * engine so the service can reach a server, snapshot its structure, and execute the single read a
4
+ * `find` call produces.
5
+ *
6
+ * **A table of dialects, not a switch.** {@link DIALECT_FACTORIES} is the whole extensibility
7
+ * story: adding PostgreSQL, MongoDB or Redis later is one entry that loads its driver and returns a
8
+ * `ManagedDialect`; `apply` needs no change, and the seam's "one dialect per engine" rule is
9
+ * enforced by `ctx.dbgraph.registerDialect` itself.
10
+ *
11
+ * **An unavailable driver disables one dialect, never the package.** SQLite lives in Node's builtins
12
+ * and MySQL's driver is an optional dependency, so either can be absent on a given installation. A
13
+ * failed load logs one line naming the engine and continues; a catalog package that refused to load
14
+ * because an unrelated driver was missing would take the working dialect down with it.
15
+ *
16
+ * **Disposal is ordered.** The plugin's connection caches are closed after the dialect
17
+ * registrations are withdrawn (effects dispose last-in-first-out), so no call can be routed to a
18
+ * dialect whose handles are already shut.
19
+ * @module dsh-plugin-dbgraph-catalog
20
+ */
21
+ import { createMysqlDialect, loadMysqlDriver } from "./mysql.js";
22
+ import { createSqliteDialect } from "./sqlite.js";
23
+ import { messageOf } from "./support.js";
24
+ export { createMysqlDialect, loadMysqlDriver, MYSQL_DRIVER_PACKAGE, mysqlIdentifier } from "./mysql.js";
25
+ export { createSqliteDialect, isStringishSqliteType, sqliteIdentifier } from "./sqlite.js";
26
+ export { HandleCache } from "./pool.js";
27
+ export { DBGRAPH_QUERY_FAILED_CODE, DBGRAPH_READ_ONLY_CODE, REDACTED_VALUE, assertReadOnlyStatement, clampRowLimit, describeWriteChannel, firstStatementKeyword, isReadStatement, messageOf, normalizeRows, renderBinary, throwIfAborted, toJsonSafe, } from "./support.js";
28
+ /** Cordis plugin name for loader diagnostics. */
29
+ export const name = 'dbgraph-catalog';
30
+ /** Services required by this plugin. */
31
+ export const inject = ['dbgraph'];
32
+ /**
33
+ * Every dialect this package can provide, in registration order.
34
+ *
35
+ * The loaders are async because MySQL's driver is imported on demand; SQLite's builtin is imported
36
+ * the same way so an older Node simply reports itself unavailable instead of crashing the module.
37
+ */
38
+ export const DIALECT_FACTORIES = [
39
+ {
40
+ engine: 'sqlite',
41
+ async load() {
42
+ const module = await import('node:sqlite');
43
+ return createSqliteDialect(module);
44
+ },
45
+ },
46
+ {
47
+ engine: 'mysql',
48
+ async load() {
49
+ return createMysqlDialect(await loadMysqlDriver());
50
+ },
51
+ },
52
+ ];
53
+ /**
54
+ * Load as many dialects as the installation supports.
55
+ *
56
+ * A failed loader is collected rather than thrown: one engine's missing optional driver must never
57
+ * prevent the other engines from registering. The split from {@link apply} exists so the
58
+ * availability decision can be tested (and reused by a deployment that mounts dialects itself)
59
+ * without constructing a Cordis context.
60
+ * @param factories - factories to try; defaults to {@link DIALECT_FACTORIES}.
61
+ * @returns the dialects that loaded, plus one failure per skipped engine.
62
+ */
63
+ export async function loadDialects(factories = DIALECT_FACTORIES) {
64
+ const dialects = [];
65
+ const failures = [];
66
+ for (const factory of factories) {
67
+ try {
68
+ dialects.push(await factory.load());
69
+ }
70
+ catch (error) {
71
+ failures.push({ engine: factory.engine, error: messageOf(error) });
72
+ }
73
+ }
74
+ return { dialects, failures };
75
+ }
76
+ /**
77
+ * Register every dialect whose driver is available.
78
+ * @param ctx - the plugin context (must inject `dbgraph`).
79
+ * @returns a promise that settles after registration (and after every failed load was reported).
80
+ */
81
+ export async function apply(ctx) {
82
+ const { dialects, failures } = await loadDialects();
83
+ for (const failure of failures) {
84
+ // One line per unavailable engine: enough for an operator to fix it, quiet enough that a
85
+ // composition with SQLite only is not noisy.
86
+ console.error(`[dbgraph-catalog] the ${failure.engine} dialect is disabled: ${failure.error}`);
87
+ }
88
+ if (dialects.length === 0)
89
+ return;
90
+ ctx.effect(function* () {
91
+ // Yielded first, disposed last: registrations are withdrawn before their handles close.
92
+ yield () => closeAll(dialects);
93
+ for (const dialect of dialects)
94
+ yield ctx.dbgraph.registerDialect(dialect);
95
+ }, 'dbgraph-catalog');
96
+ }
97
+ /**
98
+ * Close every registered dialect's handles, reporting rather than propagating a close failure.
99
+ * @param dialects - the dialects this plugin registered.
100
+ */
101
+ async function closeAll(dialects) {
102
+ for (const dialect of dialects) {
103
+ try {
104
+ await dialect.close();
105
+ }
106
+ catch (error) {
107
+ console.error(`[dbgraph-catalog] closing the ${dialect.engine} dialect failed: ${messageOf(error)}`);
108
+ }
109
+ }
110
+ }
111
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Package-owned invariant companion for `dsh-plugin-dbgraph-catalog`.
3
+ * @module dsh-plugin-dbgraph-catalog/invariant
4
+ */
5
+ import type { Context } from '@deepseek-ai/cordis';
6
+ /** Cordis companion plugin name. */
7
+ export declare const name = "dbgraph-catalog-invariant";
8
+ /** Service required before the companion can reserve package ownership. */
9
+ export declare const inject: string[];
10
+ /**
11
+ * Register this package's invariant companion.
12
+ * @param ctx - Cordis context carrying the invariant service.
13
+ * @returns the installed registration's disposer after setup succeeds.
14
+ */
15
+ export declare const apply: (ctx: Context) => Promise<() => void>;
16
+ //# sourceMappingURL=invariant.d.ts.map
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Package-owned invariant companion for `dsh-plugin-dbgraph-catalog`.
3
+ * @module dsh-plugin-dbgraph-catalog/invariant
4
+ */
5
+ const PACKAGE_NAME = 'dsh-plugin-dbgraph-catalog';
6
+ /** Cordis companion plugin name. */
7
+ export const name = 'dbgraph-catalog-invariant';
8
+ /** Service required before the companion can reserve package ownership. */
9
+ export const inject = ['invariants'];
10
+ /**
11
+ * No runtime invariant: this package owns no cross-plugin relation. Its dialect registrations live
12
+ * in `ctx.dbgraph`'s private registry and its warm connection caches are private to each dialect
13
+ * instance, torn down by the plugin's own effect-scoped disposal. There is nothing another package
14
+ * could observe and compare against.
15
+ */
16
+ const install = () => { };
17
+ /**
18
+ * Register this package's invariant companion.
19
+ * @param ctx - Cordis context carrying the invariant service.
20
+ * @returns the installed registration's disposer after setup succeeds.
21
+ */
22
+ export const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
23
+ /* jscpd:ignore-end */
24
+ //# sourceMappingURL=invariant.js.map
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Pure `information_schema` row mapping for the MySQL dialect.
3
+ *
4
+ * Kept out of `mysql.ts` on purpose: the network half can only be exercised against a live server,
5
+ * while everything that decides "what does this row mean" is a plain function over plain data. That
6
+ * is what lets the mapping be tested without pretending a mocked query proves anything about MySQL.
7
+ *
8
+ * Rows arrive as `unknown` fields because `mysql2` types `information_schema` results as
9
+ * `RowDataPacket` (an index signature), and a driver upgrade may widen a column. Every reader
10
+ * narrows; nothing here trusts a field's shape.
11
+ * @module dsh-plugin-dbgraph-catalog/mysql-map
12
+ */
13
+ import type { ConnectionConfig, RawColumn, RawEdge, RawObject } from 'dsh-plugin-dbgraph-service';
14
+ /** One `information_schema.TABLES` row, restricted to the columns the dialect selects. */
15
+ export interface MysqlTableRow {
16
+ readonly TABLE_SCHEMA?: unknown;
17
+ readonly TABLE_NAME?: unknown;
18
+ readonly TABLE_TYPE?: unknown;
19
+ readonly TABLE_COMMENT?: unknown;
20
+ readonly TABLE_ROWS?: unknown;
21
+ }
22
+ /** One `information_schema.COLUMNS` row, restricted to the columns the dialect selects. */
23
+ export interface MysqlColumnRow {
24
+ readonly TABLE_NAME?: unknown;
25
+ readonly COLUMN_NAME?: unknown;
26
+ readonly ORDINAL_POSITION?: unknown;
27
+ readonly COLUMN_TYPE?: unknown;
28
+ readonly IS_NULLABLE?: unknown;
29
+ readonly COLUMN_DEFAULT?: unknown;
30
+ readonly COLUMN_KEY?: unknown;
31
+ readonly COLUMN_COMMENT?: unknown;
32
+ }
33
+ /** One `information_schema.KEY_COLUMN_USAGE` row for a foreign key, restricted to what is needed. */
34
+ export interface MysqlForeignKeyRow {
35
+ readonly TABLE_NAME?: unknown;
36
+ readonly CONSTRAINT_NAME?: unknown;
37
+ readonly COLUMN_NAME?: unknown;
38
+ readonly ORDINAL_POSITION?: unknown;
39
+ readonly REFERENCED_TABLE_NAME?: unknown;
40
+ readonly REFERENCED_COLUMN_NAME?: unknown;
41
+ }
42
+ /** One `information_schema.STATISTICS` row, restricted to what is needed. */
43
+ export interface MysqlIndexRow {
44
+ readonly TABLE_NAME?: unknown;
45
+ readonly INDEX_NAME?: unknown;
46
+ readonly NON_UNIQUE?: unknown;
47
+ readonly SEQ_IN_INDEX?: unknown;
48
+ readonly COLUMN_NAME?: unknown;
49
+ }
50
+ /** What a `TABLES` row contributes to an object, before its columns are attached. */
51
+ export interface MysqlTableFacts {
52
+ /** Whether the row describes a base table or a view. */
53
+ readonly kind: 'table' | 'view';
54
+ /** Table comment, absent for a view's synthetic `VIEW` comment and for an empty comment. */
55
+ readonly comment?: string;
56
+ /** Approximate row count from catalog statistics; absent for views and unknown counts. */
57
+ readonly rowEstimate?: number;
58
+ }
59
+ /** Index edges and single-column uniqueness, both keyed by table name. */
60
+ export interface MysqlIndexFacts {
61
+ /** One edge per named index, columns in index order. */
62
+ readonly edges: ReadonlyMap<string, readonly RawEdge[]>;
63
+ /** Columns covered by a single-column unique index, per table. */
64
+ readonly singleUnique: ReadonlyMap<string, ReadonlySet<string>>;
65
+ }
66
+ /**
67
+ * Whether a column's declared type is string-like, and therefore worth sampling for enum values.
68
+ * MySQL reports the declared type (`varchar(64)`, `enum('a','b')`), so the base word before any
69
+ * parenthesized modifier decides.
70
+ * @param columnType - `COLUMN_TYPE` as the server declared it.
71
+ * @returns true for character, text, enum and set types.
72
+ */
73
+ export declare function isStringishMysqlType(columnType: string): boolean;
74
+ /**
75
+ * Whether one column is a candidate for enum sampling.
76
+ *
77
+ * Three gates, each for its own reason: a view's sampling query executes its definition (unbounded
78
+ * work for a catalog enrichment), a non-string column's distinct values are numbers rather than
79
+ * vocabulary, and a sensitive column's literals must never reach the on-disk index.
80
+ * @param objectKind - the object's kind (`table`, `view`, …).
81
+ * @param connection - the connection whose masking configuration applies.
82
+ * @param column - the column to screen.
83
+ * @returns true when sampling this column is safe and useful.
84
+ */
85
+ export declare function shouldSampleMysqlColumn(objectKind: string, connection: ConnectionConfig, column: RawColumn): boolean;
86
+ /**
87
+ * The join key for a comment map: a column is identified by table plus column, never by column name
88
+ * alone (two tables routinely share `id`).
89
+ * @param table - table name.
90
+ * @param column - column name.
91
+ * @returns a key that cannot collide with an ordinary name.
92
+ */
93
+ export declare function mysqlColumnKey(table: string, column: string): string;
94
+ /**
95
+ * Interpret one `TABLES` row.
96
+ *
97
+ * `TABLE_COMMENT` is `VIEW` for every view in MySQL — that is a marker, not a description, so it is
98
+ * dropped rather than shown to a model as if a human wrote it. An empty string is also no comment.
99
+ * @param row - one `information_schema.TABLES` row.
100
+ * @returns the object's kind plus whatever comment and row estimate are real.
101
+ */
102
+ export declare function mysqlTableFacts(row: MysqlTableRow): MysqlTableFacts;
103
+ /**
104
+ * Group `COLUMNS` rows into per-table column lists, in declared order.
105
+ *
106
+ * `isUnique` is deliberately not derived from `COLUMN_KEY`: MySQL reports `UNI` for the first
107
+ * column of *any* unique index, including a multi-column one, which would claim a column is unique
108
+ * on its own when it is not. L2 computes uniqueness from `STATISTICS` instead.
109
+ * @param rows - `information_schema.COLUMNS` rows.
110
+ * @returns columns keyed by table name.
111
+ */
112
+ export declare function groupMysqlColumns(rows: readonly MysqlColumnRow[]): Map<string, RawColumn[]>;
113
+ /**
114
+ * Extract column comments, keyed by {@link mysqlColumnKey}.
115
+ * @param rows - `information_schema.COLUMNS` rows that selected `COLUMN_COMMENT`.
116
+ * @returns non-empty comments only.
117
+ */
118
+ export declare function mysqlColumnComments(rows: readonly MysqlColumnRow[]): Map<string, string>;
119
+ /**
120
+ * Group `KEY_COLUMN_USAGE` rows into one edge per foreign-key constraint.
121
+ *
122
+ * `refObject` is the referenced table's plain name: a catalog describes exactly one schema, and
123
+ * `RawEdge` has no `refSchema` field, so a schema-qualified name here would not match anything the
124
+ * writer stores.
125
+ * @param rows - foreign-key rows, in any order.
126
+ * @returns edges keyed by the referencing table name.
127
+ */
128
+ export declare function groupMysqlForeignKeys(rows: readonly MysqlForeignKeyRow[]): Map<string, RawEdge[]>;
129
+ /**
130
+ * Group `STATISTICS` rows into index edges and the set of singly-unique columns.
131
+ *
132
+ * A functional index (MySQL 8) reports a `NULL` `COLUMN_NAME` and an expression instead; the seam's
133
+ * edge shape only has column names, so such an index is skipped rather than recorded with a wrong
134
+ * or empty column list.
135
+ * @param rows - `information_schema.STATISTICS` rows, in any order.
136
+ * @returns edges and single-column uniqueness, both keyed by table name.
137
+ */
138
+ export declare function groupMysqlIndexes(rows: readonly MysqlIndexRow[]): MysqlIndexFacts;
139
+ /**
140
+ * Merge one table's L1 object with its L2 facts, without mutating the input.
141
+ *
142
+ * Column comments are applied here rather than in `groupMysqlColumns` so L1 stays a structural
143
+ * snapshot and L2 is the only level that reads `COLUMN_COMMENT`.
144
+ * @param object - the L1 object.
145
+ * @param facts - what the `TABLES` row says about it, when one still exists.
146
+ * @param columnComments - comments keyed by {@link mysqlColumnKey}.
147
+ * @param uniqueColumns - columns covered by a single-column unique index.
148
+ * @param edges - foreign-key and index edges leaving this object.
149
+ * @param enumSamples - sampled literals keyed by column name.
150
+ * @returns a new object carrying the enrichment.
151
+ */
152
+ export declare function mergeMysqlObject(object: RawObject, facts: MysqlTableFacts | undefined, columnComments: ReadonlyMap<string, string>, uniqueColumns: ReadonlySet<string> | undefined, edges: readonly RawEdge[], enumSamples: ReadonlyMap<string, readonly string[]>): RawObject;
153
+ //# sourceMappingURL=mysql-map.d.ts.map
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Pure `information_schema` row mapping for the MySQL dialect.
3
+ *
4
+ * Kept out of `mysql.ts` on purpose: the network half can only be exercised against a live server,
5
+ * while everything that decides "what does this row mean" is a plain function over plain data. That
6
+ * is what lets the mapping be tested without pretending a mocked query proves anything about MySQL.
7
+ *
8
+ * Rows arrive as `unknown` fields because `mysql2` types `information_schema` results as
9
+ * `RowDataPacket` (an index signature), and a driver upgrade may widen a column. Every reader
10
+ * narrows; nothing here trusts a field's shape.
11
+ * @module dsh-plugin-dbgraph-catalog/mysql-map
12
+ */
13
+ import { asFiniteNumber, asString, isSensitiveName } from "./support.js";
14
+ /** Base type names whose values are worth sampling for enum literals. */
15
+ const STRINGISH_MYSQL_TYPES = new Set([
16
+ 'char',
17
+ 'varchar',
18
+ 'nchar',
19
+ 'nvarchar',
20
+ 'tinytext',
21
+ 'text',
22
+ 'mediumtext',
23
+ 'longtext',
24
+ 'enum',
25
+ 'set',
26
+ ]);
27
+ /**
28
+ * Whether a column's declared type is string-like, and therefore worth sampling for enum values.
29
+ * MySQL reports the declared type (`varchar(64)`, `enum('a','b')`), so the base word before any
30
+ * parenthesized modifier decides.
31
+ * @param columnType - `COLUMN_TYPE` as the server declared it.
32
+ * @returns true for character, text, enum and set types.
33
+ */
34
+ export function isStringishMysqlType(columnType) {
35
+ const base = columnType.toLowerCase().split('(')[0]?.trim() ?? '';
36
+ const word = base.split(/\s+/)[0] ?? '';
37
+ return STRINGISH_MYSQL_TYPES.has(word);
38
+ }
39
+ /**
40
+ * Whether one column is a candidate for enum sampling.
41
+ *
42
+ * Three gates, each for its own reason: a view's sampling query executes its definition (unbounded
43
+ * work for a catalog enrichment), a non-string column's distinct values are numbers rather than
44
+ * vocabulary, and a sensitive column's literals must never reach the on-disk index.
45
+ * @param objectKind - the object's kind (`table`, `view`, …).
46
+ * @param connection - the connection whose masking configuration applies.
47
+ * @param column - the column to screen.
48
+ * @returns true when sampling this column is safe and useful.
49
+ */
50
+ export function shouldSampleMysqlColumn(objectKind, connection, column) {
51
+ if (objectKind !== 'table')
52
+ return false;
53
+ if (!isStringishMysqlType(column.type))
54
+ return false;
55
+ return !isSensitiveName(connection, column.name);
56
+ }
57
+ /**
58
+ * The join key for a comment map: a column is identified by table plus column, never by column name
59
+ * alone (two tables routinely share `id`).
60
+ * @param table - table name.
61
+ * @param column - column name.
62
+ * @returns a key that cannot collide with an ordinary name.
63
+ */
64
+ export function mysqlColumnKey(table, column) {
65
+ return `${table}\u0000${column}`;
66
+ }
67
+ /**
68
+ * Interpret one `TABLES` row.
69
+ *
70
+ * `TABLE_COMMENT` is `VIEW` for every view in MySQL — that is a marker, not a description, so it is
71
+ * dropped rather than shown to a model as if a human wrote it. An empty string is also no comment.
72
+ * @param row - one `information_schema.TABLES` row.
73
+ * @returns the object's kind plus whatever comment and row estimate are real.
74
+ */
75
+ export function mysqlTableFacts(row) {
76
+ const tableType = (asString(row.TABLE_TYPE) ?? '').toUpperCase();
77
+ const kind = tableType.includes('VIEW') ? 'view' : 'table';
78
+ const rawComment = asString(row.TABLE_COMMENT)?.trim();
79
+ const comment = rawComment === undefined || rawComment === '' || (kind === 'view' && rawComment.toUpperCase() === 'VIEW')
80
+ ? undefined
81
+ : rawComment;
82
+ const rowEstimate = asFiniteNumber(row.TABLE_ROWS);
83
+ return {
84
+ kind,
85
+ ...(comment === undefined ? {} : { comment }),
86
+ ...(rowEstimate === undefined ? {} : { rowEstimate }),
87
+ };
88
+ }
89
+ /**
90
+ * Group `COLUMNS` rows into per-table column lists, in declared order.
91
+ *
92
+ * `isUnique` is deliberately not derived from `COLUMN_KEY`: MySQL reports `UNI` for the first
93
+ * column of *any* unique index, including a multi-column one, which would claim a column is unique
94
+ * on its own when it is not. L2 computes uniqueness from `STATISTICS` instead.
95
+ * @param rows - `information_schema.COLUMNS` rows.
96
+ * @returns columns keyed by table name.
97
+ */
98
+ export function groupMysqlColumns(rows) {
99
+ const grouped = new Map();
100
+ for (const row of rows) {
101
+ const table = asString(row.TABLE_NAME);
102
+ const name = asString(row.COLUMN_NAME);
103
+ if (table === undefined || name === undefined)
104
+ continue;
105
+ const list = grouped.get(table) ?? [];
106
+ const defaultValue = asString(row.COLUMN_DEFAULT);
107
+ list.push({
108
+ name,
109
+ ordinal: asFiniteNumber(row.ORDINAL_POSITION) ?? list.length + 1,
110
+ type: asString(row.COLUMN_TYPE) ?? '',
111
+ nullable: (asString(row.IS_NULLABLE) ?? 'YES').toUpperCase() === 'YES',
112
+ isPrimaryKey: (asString(row.COLUMN_KEY) ?? '').toUpperCase() === 'PRI',
113
+ ...(defaultValue === undefined ? {} : { defaultValue }),
114
+ });
115
+ grouped.set(table, list);
116
+ }
117
+ for (const [table, columns] of grouped) {
118
+ grouped.set(table, [...columns].sort((left, right) => left.ordinal - right.ordinal));
119
+ }
120
+ return grouped;
121
+ }
122
+ /**
123
+ * Extract column comments, keyed by {@link mysqlColumnKey}.
124
+ * @param rows - `information_schema.COLUMNS` rows that selected `COLUMN_COMMENT`.
125
+ * @returns non-empty comments only.
126
+ */
127
+ export function mysqlColumnComments(rows) {
128
+ const comments = new Map();
129
+ for (const row of rows) {
130
+ const table = asString(row.TABLE_NAME);
131
+ const column = asString(row.COLUMN_NAME);
132
+ const comment = asString(row.COLUMN_COMMENT)?.trim();
133
+ if (table === undefined || column === undefined || comment === undefined || comment === '')
134
+ continue;
135
+ comments.set(mysqlColumnKey(table, column), comment);
136
+ }
137
+ return comments;
138
+ }
139
+ /**
140
+ * Group `KEY_COLUMN_USAGE` rows into one edge per foreign-key constraint.
141
+ *
142
+ * `refObject` is the referenced table's plain name: a catalog describes exactly one schema, and
143
+ * `RawEdge` has no `refSchema` field, so a schema-qualified name here would not match anything the
144
+ * writer stores.
145
+ * @param rows - foreign-key rows, in any order.
146
+ * @returns edges keyed by the referencing table name.
147
+ */
148
+ export function groupMysqlForeignKeys(rows) {
149
+ const groups = new Map();
150
+ for (const row of rows) {
151
+ const table = asString(row.TABLE_NAME);
152
+ const column = asString(row.COLUMN_NAME);
153
+ const referencedTable = asString(row.REFERENCED_TABLE_NAME);
154
+ const referencedColumn = asString(row.REFERENCED_COLUMN_NAME);
155
+ if (table === undefined || column === undefined)
156
+ continue;
157
+ if (referencedTable === undefined || referencedColumn === undefined)
158
+ continue;
159
+ const name = asString(row.CONSTRAINT_NAME);
160
+ const key = `${table}\u0000${name ?? ''}`;
161
+ const group = groups.get(key) ?? { table, name, referencedTable, parts: [] };
162
+ group.parts.push({
163
+ seq: asFiniteNumber(row.ORDINAL_POSITION) ?? group.parts.length + 1,
164
+ column,
165
+ referencedColumn,
166
+ });
167
+ groups.set(key, group);
168
+ }
169
+ const edges = new Map();
170
+ for (const group of groups.values()) {
171
+ const parts = [...group.parts].sort((left, right) => left.seq - right.seq);
172
+ const list = edges.get(group.table) ?? [];
173
+ list.push({
174
+ kind: 'fk',
175
+ ...(group.name === undefined ? {} : { name: group.name }),
176
+ columns: parts.map(part => part.column),
177
+ refObject: group.referencedTable,
178
+ refColumns: parts.map(part => part.referencedColumn),
179
+ });
180
+ edges.set(group.table, list);
181
+ }
182
+ return edges;
183
+ }
184
+ /**
185
+ * Group `STATISTICS` rows into index edges and the set of singly-unique columns.
186
+ *
187
+ * A functional index (MySQL 8) reports a `NULL` `COLUMN_NAME` and an expression instead; the seam's
188
+ * edge shape only has column names, so such an index is skipped rather than recorded with a wrong
189
+ * or empty column list.
190
+ * @param rows - `information_schema.STATISTICS` rows, in any order.
191
+ * @returns edges and single-column uniqueness, both keyed by table name.
192
+ */
193
+ export function groupMysqlIndexes(rows) {
194
+ const groups = new Map();
195
+ for (const row of rows) {
196
+ const table = asString(row.TABLE_NAME);
197
+ const name = asString(row.INDEX_NAME);
198
+ if (table === undefined || name === undefined)
199
+ continue;
200
+ const key = `${table}\u0000${name}`;
201
+ const group = groups.get(key) ?? {
202
+ table,
203
+ name,
204
+ unique: asFiniteNumber(row.NON_UNIQUE) === 0,
205
+ parts: [],
206
+ };
207
+ group.parts.push({
208
+ seq: asFiniteNumber(row.SEQ_IN_INDEX) ?? group.parts.length + 1,
209
+ column: asString(row.COLUMN_NAME),
210
+ });
211
+ groups.set(key, group);
212
+ }
213
+ const edges = new Map();
214
+ const singleUnique = new Map();
215
+ for (const group of groups.values()) {
216
+ const parts = [...group.parts].sort((left, right) => left.seq - right.seq);
217
+ if (parts.some(part => part.column === undefined))
218
+ continue;
219
+ const columns = parts.map(part => part.column);
220
+ if (columns.length === 0)
221
+ continue;
222
+ const list = edges.get(group.table) ?? [];
223
+ list.push({ kind: 'index', name: group.name, columns, unique: group.unique });
224
+ edges.set(group.table, list);
225
+ const only = columns.length === 1 ? columns[0] : undefined;
226
+ if (group.unique && only !== undefined) {
227
+ const set = singleUnique.get(group.table) ?? new Set();
228
+ set.add(only);
229
+ singleUnique.set(group.table, set);
230
+ }
231
+ }
232
+ return { edges, singleUnique };
233
+ }
234
+ /**
235
+ * Merge one table's L1 object with its L2 facts, without mutating the input.
236
+ *
237
+ * Column comments are applied here rather than in `groupMysqlColumns` so L1 stays a structural
238
+ * snapshot and L2 is the only level that reads `COLUMN_COMMENT`.
239
+ * @param object - the L1 object.
240
+ * @param facts - what the `TABLES` row says about it, when one still exists.
241
+ * @param columnComments - comments keyed by {@link mysqlColumnKey}.
242
+ * @param uniqueColumns - columns covered by a single-column unique index.
243
+ * @param edges - foreign-key and index edges leaving this object.
244
+ * @param enumSamples - sampled literals keyed by column name.
245
+ * @returns a new object carrying the enrichment.
246
+ */
247
+ export function mergeMysqlObject(object, facts, columnComments, uniqueColumns, edges, enumSamples) {
248
+ const columns = object.columns.map(column => {
249
+ const comment = columnComments.get(mysqlColumnKey(object.name, column.name));
250
+ const sampled = enumSamples.get(column.name);
251
+ return {
252
+ ...column,
253
+ isUnique: uniqueColumns?.has(column.name) ?? false,
254
+ ...(comment === undefined ? {} : { comment }),
255
+ ...(sampled === undefined ? {} : { enumValues: sampled }),
256
+ };
257
+ });
258
+ return {
259
+ ...object,
260
+ ...(facts?.comment === undefined ? {} : { comment: facts.comment }),
261
+ ...(facts?.rowEstimate === undefined ? {} : { rowEstimate: facts.rowEstimate }),
262
+ columns,
263
+ ...(edges.length === 0 ? {} : { edges }),
264
+ };
265
+ }
266
+ //# sourceMappingURL=mysql-map.js.map