metal-orm 1.0.50 → 1.0.51

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,391 @@
1
+ import {
2
+ applyToCompoundHead,
3
+ normalizeLookup,
4
+ stripDiacritics
5
+ } from './compound.mjs';
6
+
7
+ // ═══════════════════════════════════════════════════════════════════════════
8
+ // PATTERNS
9
+ // ═══════════════════════════════════════════════════════════════════════════
10
+
11
+ /**
12
+ * Precompiled regex patterns for performance.
13
+ * @type {Readonly<Record<string, RegExp>>}
14
+ */
15
+ const PATTERNS = Object.freeze({
16
+ consonantEnding: /[rzn]$/,
17
+ consonantEsEnding: /[rzn]es$/,
18
+ endsInX: /x$/,
19
+ vowelBeforeS: /[aeiou]s$/,
20
+ });
21
+
22
+ // ═══════════════════════════════════════════════════════════════════════════
23
+ // IRREGULAR DICTIONARIES (all normalized - no diacritics)
24
+ // ═══════════════════════════════════════════════════════════════════════════
25
+
26
+ /**
27
+ * Default irregular plurals for Brazilian Portuguese.
28
+ * Keys AND values are normalized (no diacritics, lowercase).
29
+ * @type {Readonly<Record<string, string>>}
30
+ */
31
+ export const PT_BR_DEFAULT_IRREGULARS = Object.freeze({
32
+ // ─────────────────────────────────────────────────────────────────────────
33
+ // -ão → -ães (irregular, must memorize)
34
+ // ─────────────────────────────────────────────────────────────────────────
35
+ 'pao': 'paes',
36
+ 'cao': 'caes',
37
+ 'alemao': 'alemaes',
38
+ 'capitao': 'capitaes',
39
+ 'charlatao': 'charlataes',
40
+ 'escrivao': 'escrivaes',
41
+ 'tabeliao': 'tabeliaes',
42
+ 'guardiao': 'guardiaes',
43
+ 'sacristao': 'sacristaes',
44
+
45
+ // ─────────────────────────────────────────────────────────────────────────
46
+ // -ão → -ãos (irregular, must memorize)
47
+ // ─────────────────────────────────────────────────────────────────────────
48
+ 'mao': 'maos',
49
+ 'cidadao': 'cidadaos',
50
+ 'cristao': 'cristaos',
51
+ 'irmao': 'irmaos',
52
+ 'orgao': 'orgaos',
53
+ 'bencao': 'bencaos',
54
+ 'grao': 'graos',
55
+ 'orfao': 'orfaos',
56
+ 'sotao': 'sotaos',
57
+ 'acordao': 'acordaos',
58
+ 'cortesao': 'cortesaos',
59
+ 'pagao': 'pagaos',
60
+ 'chao': 'chaos',
61
+ 'vao': 'vaos',
62
+
63
+ // ─────────────────────────────────────────────────────────────────────────
64
+ // -l special cases
65
+ // ─────────────────────────────────────────────────────────────────────────
66
+ 'mal': 'males',
67
+ 'consul': 'consules',
68
+
69
+ // ─────────────────────────────────────────────────────────────────────────
70
+ // Unstressed -il → -eis (paroxytones)
71
+ // ─────────────────────────────────────────────────────────────────────────
72
+ 'fossil': 'fosseis',
73
+ 'reptil': 'repteis',
74
+ 'facil': 'faceis',
75
+ 'dificil': 'dificeis',
76
+ 'util': 'uteis',
77
+ 'inutil': 'inuteis',
78
+ 'agil': 'ageis',
79
+ 'fragil': 'frageis',
80
+ 'projetil': 'projeteis',
81
+ 'volatil': 'volateis',
82
+ 'docil': 'doceis',
83
+ 'portatil': 'portateis',
84
+ 'textil': 'texteis',
85
+
86
+ // ─────────────────────────────────────────────────────────────────────────
87
+ // Invariable words (paroxytone/proparoxytone ending in -s/-x)
88
+ // ─────────────────────────────────────────────────────────────────────────
89
+ 'onibus': 'onibus',
90
+ 'lapis': 'lapis',
91
+ 'virus': 'virus',
92
+ 'atlas': 'atlas',
93
+ 'pires': 'pires',
94
+ 'cais': 'cais',
95
+ 'torax': 'torax',
96
+ 'fenix': 'fenix',
97
+ 'xerox': 'xerox',
98
+ 'latex': 'latex',
99
+ 'index': 'index',
100
+ 'duplex': 'duplex',
101
+ 'telex': 'telex',
102
+ 'climax': 'climax',
103
+ 'simples': 'simples',
104
+ 'oasis': 'oasis',
105
+ 'tenis': 'tenis',
106
+
107
+ // ─────────────────────────────────────────────────────────────────────────
108
+ // -ês → -eses (nationalities, months, etc.)
109
+ // ─────────────────────────────────────────────────────────────────────────
110
+ 'portugues': 'portugueses',
111
+ 'ingles': 'ingleses',
112
+ 'frances': 'franceses',
113
+ 'holandes': 'holandeses',
114
+ 'japones': 'japoneses',
115
+ 'chines': 'chineses',
116
+ 'irlandes': 'irlandeses',
117
+ 'escoces': 'escoceses',
118
+ 'mes': 'meses',
119
+ 'burges': 'burgueses',
120
+ 'fregues': 'fregueses',
121
+ 'marques': 'marqueses',
122
+
123
+ // ─────────────────────────────────────────────────────────────────────────
124
+ // Other irregulars
125
+ // ─────────────────────────────────────────────────────────────────────────
126
+ 'qualquer': 'quaisquer',
127
+ 'carater': 'caracteres',
128
+ 'junior': 'juniores',
129
+ 'senior': 'seniores',
130
+ });
131
+
132
+ /**
133
+ * Builds reverse irregular mapping (plural → singular).
134
+ * @param {Record<string, string>} irregulars
135
+ * @returns {Record<string, string>}
136
+ */
137
+ const buildSingularIrregulars = (irregulars) => {
138
+ const result = {};
139
+ for (const [singular, plural] of Object.entries(irregulars)) {
140
+ if (plural !== singular) {
141
+ result[plural] = singular;
142
+ }
143
+ }
144
+ return result;
145
+ };
146
+
147
+ /**
148
+ * Default irregular singulars (auto-generated reverse mapping).
149
+ * @type {Readonly<Record<string, string>>}
150
+ */
151
+ export const PT_BR_DEFAULT_SINGULAR_IRREGULARS = Object.freeze(
152
+ buildSingularIrregulars(PT_BR_DEFAULT_IRREGULARS)
153
+ );
154
+
155
+ // ═══════════════════════════════════════════════════════════════════════════
156
+ // CONNECTORS
157
+ // ═══════════════════════════════════════════════════════════════════════════
158
+
159
+ /**
160
+ * Portuguese connector words used in compound expressions.
161
+ * @type {ReadonlySet<string>}
162
+ */
163
+ export const PT_BR_CONNECTORS = Object.freeze(new Set(
164
+ [
165
+ 'de', 'da', 'do', 'das', 'dos',
166
+ 'em', 'na', 'no', 'nas', 'nos',
167
+ 'a', 'ao', 'as', 'aos',
168
+ 'com', 'sem', 'sob', 'sobre',
169
+ 'para', 'por', 'pela', 'pelo', 'pelas', 'pelos',
170
+ 'entre', 'contra', 'perante',
171
+ 'e', 'ou'
172
+ ].map(normalizeLookup)
173
+ ));
174
+
175
+ // ═══════════════════════════════════════════════════════════════════════════
176
+ // INFLECTION RULES
177
+ // ═══════════════════════════════════════════════════════════════════════════
178
+
179
+ /**
180
+ * Pluralization rules for Portuguese words.
181
+ * Format: [suffix, replacement, suffixLength]
182
+ * @type {ReadonlyArray<Readonly<[string, string, number]>>}
183
+ */
184
+ const PLURAL_RULES = Object.freeze([
185
+ // -ão → -ões (default; -ães and -ãos handled via irregulars)
186
+ ['ao', 'oes', 2],
187
+ // -m → -ns
188
+ ['m', 'ns', 1],
189
+ // -l endings
190
+ ['al', 'ais', 2],
191
+ ['el', 'eis', 2],
192
+ ['ol', 'ois', 2],
193
+ ['ul', 'uis', 2],
194
+ ['il', 'is', 2], // Stressed -il; unstressed in irregulars
195
+ ]);
196
+
197
+ /**
198
+ * Singularization rules for Portuguese words.
199
+ * Format: [suffix, replacement, suffixLength]
200
+ * @type {ReadonlyArray<Readonly<[string, string, number]>>}
201
+ */
202
+ const SINGULAR_RULES = Object.freeze([
203
+ // -ões/-ães/-ãos → -ão
204
+ ['oes', 'ao', 3],
205
+ ['aes', 'ao', 3],
206
+ ['aos', 'ao', 3],
207
+ // -ns → -m
208
+ ['ns', 'm', 2],
209
+ // -l endings reverse
210
+ ['ais', 'al', 3],
211
+ ['eis', 'el', 3],
212
+ ['ois', 'ol', 3],
213
+ ['uis', 'ul', 3],
214
+ ['is', 'il', 2],
215
+ // -eses → -es
216
+ ['eses', 'es', 4],
217
+ ]);
218
+
219
+ // ═══════════════════════════════════════════════════════════════════════════
220
+ // UTILITY FUNCTIONS
221
+ // ═══════════════════════════════════════════════════════════════════════════
222
+
223
+ /**
224
+ * Normalizes a word for rule matching and irregular lookup.
225
+ * @param {string} word - The word to normalize
226
+ * @returns {string} Normalized word (lowercase, no diacritics)
227
+ */
228
+ const normalizeWord = (word) =>
229
+ stripDiacritics((word ?? '').toString()).toLowerCase().trim();
230
+
231
+ /**
232
+ * Applies suffix rules to a word.
233
+ * @param {string} word - Normalized word
234
+ * @param {ReadonlyArray<Readonly<[string, string, number]>>} rules
235
+ * @returns {string|null} Transformed word or null if no rule matched
236
+ */
237
+ const applyRules = (word, rules) => {
238
+ for (const [suffix, replacement, length] of rules) {
239
+ if (word.endsWith(suffix)) {
240
+ return word.slice(0, -length) + replacement;
241
+ }
242
+ }
243
+ return null;
244
+ };
245
+
246
+ // ═══════════════════════════════════════════════════════════════════════════
247
+ // PLURALIZATION
248
+ // ═══════════════════════════════════════════════════════════════════════════
249
+
250
+ /**
251
+ * Converts a Portuguese word to its plural form.
252
+ * Output is normalized (no diacritics, lowercase).
253
+ *
254
+ * @param {string} word - The word to pluralize
255
+ * @param {Record<string, string>} [irregulars=PT_BR_DEFAULT_IRREGULARS]
256
+ * @returns {string} The pluralized word (normalized)
257
+ */
258
+ export const pluralizeWordPtBr = (
259
+ word,
260
+ irregulars = PT_BR_DEFAULT_IRREGULARS
261
+ ) => {
262
+ const normalized = normalizeWord(word);
263
+ if (!normalized) return '';
264
+
265
+ // 1. Check irregulars first
266
+ const irregular = irregulars[normalized];
267
+ if (irregular !== undefined) {
268
+ return irregular;
269
+ }
270
+
271
+ // 2. Apply suffix-based rules
272
+ const ruleResult = applyRules(normalized, PLURAL_RULES);
273
+ if (ruleResult !== null) {
274
+ return ruleResult;
275
+ }
276
+
277
+ // 3. Words ending in -x are typically invariable
278
+ if (PATTERNS.endsInX.test(normalized)) {
279
+ return normalized;
280
+ }
281
+
282
+ // 4. Consonants r, z, n require -es
283
+ if (PATTERNS.consonantEnding.test(normalized)) {
284
+ return normalized + 'es';
285
+ }
286
+
287
+ // 5. Words ending in -s (invariable or already plural)
288
+ if (normalized.endsWith('s')) {
289
+ return normalized;
290
+ }
291
+
292
+ // 6. Default: add -s
293
+ return normalized + 's';
294
+ };
295
+
296
+ // ═══════════════════════════════════════════════════════════════════════════
297
+ // SINGULARIZATION
298
+ // ═══════════════════════════════════════════════════════════════════════════
299
+
300
+ /**
301
+ * Converts a Portuguese word to its singular form.
302
+ * Output is normalized (no diacritics, lowercase).
303
+ *
304
+ * @param {string} word - The word to singularize
305
+ * @param {Record<string, string>} [irregulars=PT_BR_DEFAULT_SINGULAR_IRREGULARS]
306
+ * @returns {string} The singularized word (normalized)
307
+ */
308
+ export const singularizeWordPtBr = (
309
+ word,
310
+ irregulars = PT_BR_DEFAULT_SINGULAR_IRREGULARS
311
+ ) => {
312
+ const normalized = normalizeWord(word);
313
+ if (!normalized) return '';
314
+
315
+ // 1. Check irregulars first
316
+ const irregular = irregulars[normalized];
317
+ if (irregular !== undefined) {
318
+ return irregular;
319
+ }
320
+
321
+ // 2. Apply suffix-based rules
322
+ const ruleResult = applyRules(normalized, SINGULAR_RULES);
323
+ if (ruleResult !== null) {
324
+ return ruleResult;
325
+ }
326
+
327
+ // 3. Handle consonant + es pattern
328
+ if (PATTERNS.consonantEsEnding.test(normalized)) {
329
+ return normalized.slice(0, -2);
330
+ }
331
+
332
+ // 4. Words ending in vowel+s: remove s
333
+ if (PATTERNS.vowelBeforeS.test(normalized)) {
334
+ return normalized.slice(0, -1);
335
+ }
336
+
337
+ // 5. Already singular or invariable
338
+ return normalized;
339
+ };
340
+
341
+ // ═══════════════════════════════════════════════════════════════════════════
342
+ // COMPOUND TERM HANDLING
343
+ // ═══════════════════════════════════════════════════════════════════════════
344
+
345
+ /**
346
+ * Pluralizes a compound property/relation name in Portuguese.
347
+ */
348
+ export const pluralizeRelationPropertyPtBr = (
349
+ term,
350
+ { pluralizeWord = pluralizeWordPtBr, connectors = PT_BR_CONNECTORS } = {}
351
+ ) => applyToCompoundHead(term, { connectors, transformWord: pluralizeWord });
352
+
353
+ /**
354
+ * Singularizes a compound property/relation name in Portuguese.
355
+ */
356
+ export const singularizeRelationPropertyPtBr = (
357
+ term,
358
+ { singularizeWord = singularizeWordPtBr, connectors = PT_BR_CONNECTORS } = {}
359
+ ) => applyToCompoundHead(term, { connectors, transformWord: singularizeWord });
360
+
361
+ // ═══════════════════════════════════════════════════════════════════════════
362
+ // INFLECTOR FACTORY
363
+ // ═══════════════════════════════════════════════════════════════════════════
364
+
365
+ /**
366
+ * Creates a Brazilian Portuguese inflector instance.
367
+ */
368
+ export const createPtBrInflector = ({ customIrregulars = {} } = {}) => {
369
+ const irregularPlurals = Object.freeze({
370
+ ...PT_BR_DEFAULT_IRREGULARS,
371
+ ...customIrregulars
372
+ });
373
+
374
+ const irregularSingulars = Object.freeze({
375
+ ...PT_BR_DEFAULT_SINGULAR_IRREGULARS,
376
+ ...buildSingularIrregulars(customIrregulars)
377
+ });
378
+
379
+ return Object.freeze({
380
+ locale: 'pt-BR',
381
+ irregularPlurals,
382
+ irregularSingulars,
383
+ pluralizeWord: (w) => pluralizeWordPtBr(w, irregularPlurals),
384
+ singularizeWord: (w) => singularizeWordPtBr(w, irregularSingulars),
385
+ pluralizeRelationProperty: pluralizeRelationPropertyPtBr,
386
+ singularizeRelationProperty: singularizeRelationPropertyPtBr,
387
+ normalizeForLookup: normalizeWord
388
+ });
389
+ };
390
+
391
+ export default createPtBrInflector;
@@ -1,18 +1,23 @@
1
+ import { resolveInflector } from './inflection/index.mjs';
2
+
1
3
  export class BaseNamingStrategy {
2
- constructor(irregulars = {}) {
4
+ constructor(irregulars = {}, inflector = resolveInflector('en')) {
3
5
  this.irregulars = new Map();
4
6
  this.inverseIrregulars = new Map();
7
+ this.inflector = inflector;
5
8
  for (const [singular, plural] of Object.entries(irregulars)) {
6
9
  if (!singular || !plural) continue;
7
- const singularKey = singular.toLowerCase();
8
- const pluralValue = plural.toLowerCase();
10
+ const normalize = this.inflector.normalizeForIrregularKey || (value => String(value).toLowerCase());
11
+ const singularKey = normalize(singular);
12
+ const pluralValue = normalize(plural);
9
13
  this.irregulars.set(singularKey, pluralValue);
10
14
  this.inverseIrregulars.set(pluralValue, singularKey);
11
15
  }
12
16
  }
13
17
 
14
18
  applyIrregular(word, direction) {
15
- const lower = word.toLowerCase();
19
+ const normalize = this.inflector.normalizeForIrregularKey || (value => String(value).toLowerCase());
20
+ const lower = normalize(word);
16
21
  if (direction === 'plural' && this.irregulars.has(lower)) {
17
22
  return this.irregulars.get(lower);
18
23
  }
@@ -49,20 +54,13 @@ export class BaseNamingStrategy {
49
54
  pluralize(word) {
50
55
  const irregular = this.applyIrregular(word, 'plural');
51
56
  if (irregular) return irregular;
52
- const lower = word.toLowerCase();
53
- if (lower.endsWith('y')) return `${lower.slice(0, -1)}ies`;
54
- if (lower.endsWith('s')) return `${lower}es`;
55
- return `${lower}s`;
57
+ return this.inflector.pluralizeWord(word);
56
58
  }
57
59
 
58
60
  singularize(word) {
59
61
  const irregular = this.applyIrregular(word, 'singular');
60
62
  if (irregular) return irregular;
61
- const lower = word.toLowerCase();
62
- if (lower.endsWith('ies')) return `${lower.slice(0, -3)}y`;
63
- if (lower.endsWith('ses')) return lower.slice(0, -2);
64
- if (lower.endsWith('s')) return lower.slice(0, -1);
65
- return lower;
63
+ return this.inflector.singularizeWord(word);
66
64
  }
67
65
 
68
66
  classNameFromTable(tableName) {
@@ -76,7 +74,11 @@ export class BaseNamingStrategy {
76
74
  }
77
75
 
78
76
  hasManyProperty(targetTable) {
79
- return this.toCamelCase(this.pluralize(this.singularize(targetTable)));
77
+ const base = this.singularize(targetTable);
78
+ const plural = this.inflector.pluralizeRelationProperty
79
+ ? this.inflector.pluralizeRelationProperty(base, { pluralizeWord: word => this.pluralize(word) })
80
+ : this.pluralize(base);
81
+ return this.toCamelCase(plural);
80
82
  }
81
83
 
82
84
  hasOneProperty(targetTable) {
@@ -84,7 +86,7 @@ export class BaseNamingStrategy {
84
86
  }
85
87
 
86
88
  belongsToManyProperty(targetTable) {
87
- return this.toCamelCase(this.pluralize(this.singularize(targetTable)));
89
+ return this.hasManyProperty(targetTable);
88
90
  }
89
91
 
90
92
  defaultTableNameFromClass(className) {
@@ -94,59 +96,21 @@ export class BaseNamingStrategy {
94
96
  }
95
97
  }
96
98
 
97
- export class EnglishNamingStrategy extends BaseNamingStrategy {}
98
-
99
- const DEFAULT_PT_IRREGULARS = {
100
- mao: 'maos',
101
- pao: 'paes',
102
- cao: 'caes',
103
- mal: 'males',
104
- consul: 'consules'
105
- };
106
-
107
- export class PortugueseNamingStrategy extends BaseNamingStrategy {
99
+ export class EnglishNamingStrategy extends BaseNamingStrategy {
108
100
  constructor(irregulars = {}) {
109
- super({ ...DEFAULT_PT_IRREGULARS, ...irregulars });
110
- }
111
-
112
- pluralize(word) {
113
- const irregular = this.applyIrregular(word, 'plural');
114
- if (irregular) return irregular;
115
- const lower = word.toLowerCase();
116
- if (lower.endsWith('cao')) return `${lower.slice(0, -3)}coes`;
117
- if (lower.endsWith('ao')) return `${lower.slice(0, -2)}oes`;
118
- if (lower.endsWith('m')) return `${lower.slice(0, -1)}ns`;
119
- if (lower.endsWith('al')) return `${lower.slice(0, -2)}ais`;
120
- if (lower.endsWith('el')) return `${lower.slice(0, -2)}eis`;
121
- if (lower.endsWith('ol')) return `${lower.slice(0, -2)}ois`;
122
- if (lower.endsWith('ul')) return `${lower.slice(0, -2)}uis`;
123
- if (lower.endsWith('il')) return `${lower.slice(0, -2)}is`;
124
- if (/[rznsx]$/.test(lower)) return `${lower}es`;
125
- if (lower.endsWith('s')) return lower;
126
- return `${lower}s`;
101
+ super(irregulars, resolveInflector('en'));
127
102
  }
103
+ }
128
104
 
129
- singularize(word) {
130
- const irregular = this.applyIrregular(word, 'singular');
131
- if (irregular) return irregular;
132
- const lower = word.toLowerCase();
133
- if (lower.endsWith('coes')) return `${lower.slice(0, -4)}cao`;
134
- if (lower.endsWith('oes')) return `${lower.slice(0, -3)}ao`;
135
- if (lower.endsWith('ns')) return `${lower.slice(0, -2)}m`;
136
- if (lower.endsWith('ais')) return `${lower.slice(0, -3)}al`;
137
- if (lower.endsWith('eis')) return `${lower.slice(0, -3)}el`;
138
- if (lower.endsWith('ois')) return `${lower.slice(0, -3)}ol`;
139
- if (lower.endsWith('uis')) return `${lower.slice(0, -3)}ul`;
140
- if (lower.endsWith('is')) return `${lower.slice(0, -2)}il`;
141
- if (/[rznsx]es$/.test(lower)) return lower.replace(/es$/, '');
142
- if (lower.endsWith('s')) return lower.slice(0, -1);
143
- return lower;
105
+ export class PortugueseNamingStrategy extends BaseNamingStrategy {
106
+ constructor(irregulars = {}) {
107
+ const inflector = resolveInflector('pt-BR');
108
+ super({ ...inflector.defaultIrregulars, ...irregulars }, inflector);
144
109
  }
145
110
  }
146
111
 
147
112
  export const createNamingStrategy = (locale = 'en', irregulars) => {
148
- const normalized = (locale || 'en').toLowerCase();
149
- if (normalized.startsWith('pt')) return new PortugueseNamingStrategy(irregulars);
150
- if (normalized.startsWith('en')) return new EnglishNamingStrategy(irregulars);
151
- return new EnglishNamingStrategy(irregulars);
113
+ const inflector = resolveInflector(locale);
114
+ const mergedIrregulars = { ...(inflector.defaultIrregulars || {}), ...(irregulars || {}) };
115
+ return new BaseNamingStrategy(mergedIrregulars, inflector);
152
116
  };
@@ -0,0 +1,19 @@
1
+ export {
2
+ stripDiacritics,
3
+ normalizeLookup,
4
+ detectTextFormat,
5
+ splitIntoWords,
6
+ rebuildFromWords,
7
+ applyToCompoundHead
8
+ } from './inflection/compound.mjs';
9
+
10
+ import {
11
+ PT_BR_CONNECTORS as DEFAULT_CONNECTORS,
12
+ pluralizeWordPtBr,
13
+ pluralizeRelationPropertyPtBr as pluralizeCompoundHead
14
+ } from './inflection/pt-br.mjs';
15
+
16
+ export { DEFAULT_CONNECTORS, pluralizeWordPtBr, pluralizeCompoundHead };
17
+
18
+ export const pluralizeTerm = pluralizeCompoundHead;
19
+ export default pluralizeTerm;
@@ -1,6 +1,6 @@
1
1
  import type { ReferentialAction } from '../../../schema/column-types.js';
2
2
  import { SchemaIntrospector, IntrospectOptions } from './types.js';
3
- import { shouldIncludeTable } from './utils.js';
3
+ import { shouldIncludeTable, queryRows } from './utils.js';
4
4
  import { DatabaseSchema, DatabaseTable, DatabaseIndex, DatabaseColumn } from '../schema-types.js';
5
5
  import type { IntrospectContext } from './context.js';
6
6
  import { runSelectNode } from './run-select.js';
@@ -67,6 +67,19 @@ type MssqlForeignKeyRow = {
67
67
  update_rule: string | null;
68
68
  };
69
69
 
70
+ type MssqlTableCommentRow = {
71
+ table_schema: string;
72
+ table_name: string;
73
+ comment: string | null;
74
+ };
75
+
76
+ type MssqlColumnCommentRow = {
77
+ table_schema: string;
78
+ table_name: string;
79
+ column_name: string;
80
+ comment: string | null;
81
+ };
82
+
70
83
  type ForeignKeyEntry = {
71
84
  table: string;
72
85
  column: string;
@@ -107,6 +120,60 @@ export const mssqlIntrospector: SchemaIntrospector = {
107
120
  async introspect(ctx: IntrospectContext, options: IntrospectOptions): Promise<DatabaseSchema> {
108
121
  const schema = options.schema;
109
122
  const schemaCondition = schema ? eq(columnNode('sch', 'name'), schema) : undefined;
123
+ const schemaFilter = schema ? 'AND sch.name = @p1' : '';
124
+ const schemaParams = schema ? [schema] : [];
125
+ const tableCommentRows = (await queryRows(
126
+ ctx.executor,
127
+ `
128
+ SELECT
129
+ sch.name AS table_schema,
130
+ t.name AS table_name,
131
+ CONVERT(nvarchar(4000), ep.value) AS comment
132
+ FROM sys.extended_properties ep
133
+ JOIN sys.tables t ON t.object_id = ep.major_id
134
+ JOIN sys.schemas sch ON sch.schema_id = t.schema_id
135
+ WHERE ep.class = 1
136
+ AND ep.minor_id = 0
137
+ AND ep.name = 'MS_Description'
138
+ ${schemaFilter}
139
+ `,
140
+ schemaParams
141
+ )) as MssqlTableCommentRow[];
142
+ const columnCommentRows = (await queryRows(
143
+ ctx.executor,
144
+ `
145
+ SELECT
146
+ sch.name AS table_schema,
147
+ t.name AS table_name,
148
+ col.name AS column_name,
149
+ CONVERT(nvarchar(4000), ep.value) AS comment
150
+ FROM sys.extended_properties ep
151
+ JOIN sys.columns col ON col.object_id = ep.major_id AND col.column_id = ep.minor_id
152
+ JOIN sys.tables t ON t.object_id = col.object_id
153
+ JOIN sys.schemas sch ON sch.schema_id = t.schema_id
154
+ WHERE ep.class = 1
155
+ AND ep.minor_id > 0
156
+ AND ep.name = 'MS_Description'
157
+ ${schemaFilter}
158
+ `,
159
+ schemaParams
160
+ )) as MssqlColumnCommentRow[];
161
+ const tableComments = new Map<string, string>();
162
+ tableCommentRows.forEach(r => {
163
+ if (!shouldIncludeTable(r.table_name, options)) return;
164
+ if (!r.comment) return;
165
+ const trimmed = r.comment.trim();
166
+ if (!trimmed) return;
167
+ tableComments.set(`${r.table_schema}.${r.table_name}`, trimmed);
168
+ });
169
+ const columnComments = new Map<string, string>();
170
+ columnCommentRows.forEach(r => {
171
+ if (!shouldIncludeTable(r.table_name, options)) return;
172
+ if (!r.comment) return;
173
+ const trimmed = r.comment.trim();
174
+ if (!trimmed) return;
175
+ columnComments.set(`${r.table_schema}.${r.table_name}.${r.column_name}`, trimmed);
176
+ });
110
177
 
111
178
  const dataTypeExpression = buildMssqlDataType(
112
179
  { table: 'ty', name: 'name' },
@@ -425,7 +492,8 @@ export const mssqlIntrospector: SchemaIntrospector = {
425
492
  schema: r.table_schema,
426
493
  columns: [],
427
494
  primaryKey: pkMap.get(key) || [],
428
- indexes: []
495
+ indexes: [],
496
+ comment: tableComments.get(key)
429
497
  });
430
498
  }
431
499
  const table = tablesByKey.get(key)!;
@@ -436,6 +504,10 @@ export const mssqlIntrospector: SchemaIntrospector = {
436
504
  default: r.column_default ?? undefined,
437
505
  autoIncrement: !!r.is_identity
438
506
  };
507
+ const columnComment = columnComments.get(`${key}.${r.column_name}`);
508
+ if (columnComment) {
509
+ column.comment = columnComment;
510
+ }
439
511
  const fk = fkMap.get(`${key}.${r.column_name}`)?.[0];
440
512
  if (fk) {
441
513
  column.references = {