metal-orm 1.0.46 → 1.0.48

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metal-orm",
3
- "version": "1.0.46",
3
+ "version": "1.0.48",
4
4
  "type": "module",
5
5
  "types": "./dist/index.d.ts",
6
6
  "engines": {
@@ -23,6 +23,7 @@ import {
23
23
  createSqliteExecutor,
24
24
  createMssqlExecutor
25
25
  } from '../dist/index.js';
26
+ import { createNamingStrategy } from './naming-strategy.mjs';
26
27
 
27
28
  const pkgVersion = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
28
29
 
@@ -41,6 +42,8 @@ const parseArgs = () => {
41
42
  include: { type: 'string' },
42
43
  exclude: { type: 'string' },
43
44
  out: { type: 'string' },
45
+ locale: { type: 'string' },
46
+ 'naming-overrides': { type: 'string' },
44
47
  'dry-run': { type: 'boolean' },
45
48
  help: { type: 'boolean', short: 'h' },
46
49
  version: { type: 'boolean' }
@@ -70,6 +73,10 @@ const parseArgs = () => {
70
73
  include: values.include ? values.include.split(',').map(v => v.trim()).filter(Boolean) : undefined,
71
74
  exclude: values.exclude ? values.exclude.split(',').map(v => v.trim()).filter(Boolean) : undefined,
72
75
  out: values.out ? path.resolve(process.cwd(), values.out) : path.join(process.cwd(), 'generated-entities.ts'),
76
+ locale: (values.locale || 'en').toLowerCase(),
77
+ namingOverrides: values['naming-overrides']
78
+ ? path.resolve(process.cwd(), values['naming-overrides'])
79
+ : undefined,
73
80
  dryRun: Boolean(values['dry-run'])
74
81
  };
75
82
 
@@ -102,6 +109,8 @@ Usage:
102
109
  Flags:
103
110
  --include=tbl1,tbl2 Only include these tables
104
111
  --exclude=tbl3,tbl4 Exclude these tables
112
+ --locale=pt-BR Naming locale for class/relation names (default: en)
113
+ --naming-overrides Path to JSON map of irregular plurals { "singular": "plural" }
105
114
  --dry-run Print to stdout instead of writing a file
106
115
  --help Show this help
107
116
  `
@@ -110,56 +119,26 @@ Flags:
110
119
 
111
120
  const escapeJsString = value => value.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
112
121
 
113
- const toPascalCase = value =>
114
- value
115
- .split(/[^a-zA-Z0-9]+/)
116
- .filter(Boolean)
117
- .map(part => part.charAt(0).toUpperCase() + part.slice(1))
118
- .join('') || 'Entity';
119
-
120
- const toCamelCase = value => {
121
- const pascal = toPascalCase(value);
122
- return pascal.charAt(0).toLowerCase() + pascal.slice(1);
123
- };
124
-
125
- const singularize = name => {
126
- if (name.endsWith('ies')) return name.slice(0, -3) + 'y';
127
- if (name.endsWith('ses')) return name.slice(0, -2);
128
- if (name.endsWith('s')) return name.slice(0, -1);
129
- return name;
130
- };
131
-
132
- const pluralize = name => {
133
- if (name.endsWith('y')) return `${name.slice(0, -1)}ies`;
134
- if (name.endsWith('s')) return `${name}es`;
135
- return `${name}s`;
136
- };
137
-
138
- const deriveClassName = tableName => toPascalCase(singularize(tableName));
139
-
140
- const toSnakeCase = value =>
141
- value
142
- .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
143
- .replace(/[^a-z0-9_]+/gi, '_')
144
- .replace(/__+/g, '_')
145
- .replace(/^_|_$/g, '')
146
- .toLowerCase();
147
-
148
- const deriveDefaultTableNameFromClass = className => {
149
- const normalized = toSnakeCase(className);
150
- if (!normalized) return 'unknown';
151
- return normalized.endsWith('s') ? normalized : `${normalized}s`;
152
- };
153
-
154
- const deriveBelongsToName = (fkName, targetTable) => {
155
- const trimmed = fkName.replace(/_?id$/i, '');
156
- const base = trimmed && trimmed !== fkName ? trimmed : singularize(targetTable);
157
- return toCamelCase(base);
122
+ const loadIrregulars = async filePath => {
123
+ const raw = await fs.readFile(filePath, 'utf8');
124
+ let parsed;
125
+ try {
126
+ parsed = JSON.parse(raw);
127
+ } catch (err) {
128
+ throw new Error(`Failed to parse naming overrides at ${filePath}: ${err.message || err}`);
129
+ }
130
+ const irregulars =
131
+ parsed && typeof parsed === 'object' && !Array.isArray(parsed)
132
+ ? parsed.irregulars && typeof parsed.irregulars === 'object' && !Array.isArray(parsed.irregulars)
133
+ ? parsed.irregulars
134
+ : parsed
135
+ : undefined;
136
+ if (!irregulars) {
137
+ throw new Error(`Naming overrides at ${filePath} must be an object or { "irregulars": { ... } }`);
138
+ }
139
+ return irregulars;
158
140
  };
159
141
 
160
- const deriveHasManyName = targetTable => toCamelCase(pluralize(targetTable));
161
- const deriveBelongsToManyName = targetTable => toCamelCase(pluralize(targetTable));
162
-
163
142
  const parseColumnType = colTypeRaw => {
164
143
  const type = (colTypeRaw || '').toLowerCase();
165
144
  const lengthMatch = type.match(/\((\d+)(?:\s*,\s*(\d+))?\)/);
@@ -266,7 +245,8 @@ const renderColumnExpression = (column, tablePk) => {
266
245
  };
267
246
  };
268
247
 
269
- const mapRelations = tables => {
248
+ const mapRelations = (tables, naming) => {
249
+ const normalizeName = name => (typeof name === 'string' && name.includes('.') ? name.split('.').pop() : name);
270
250
  const relationMap = new Map();
271
251
  const relationKeys = new Map();
272
252
  const fkIndex = new Map();
@@ -283,12 +263,15 @@ const mapRelations = tables => {
283
263
  }
284
264
  }
285
265
 
286
- const findTable = name => tables.find(t => t.name === name);
266
+ const findTable = name => {
267
+ const norm = normalizeName(name);
268
+ return tables.find(t => t.name === name || t.name === norm);
269
+ };
287
270
 
288
271
  const pivotTables = new Set();
289
272
  for (const table of tables) {
290
273
  const fkCols = fkIndex.get(table.name) || [];
291
- const distinctTargets = Array.from(new Set(fkCols.map(c => c.references.table)));
274
+ const distinctTargets = Array.from(new Set(fkCols.map(c => normalizeName(c.references.table))));
292
275
  if (fkCols.length === 2 && distinctTargets.length === 2) {
293
276
  const [a, b] = fkCols;
294
277
  pivotTables.add(table.name);
@@ -297,8 +280,8 @@ const mapRelations = tables => {
297
280
  if (targetA && targetB) {
298
281
  const aKey = relationKeys.get(targetA.name);
299
282
  const bKey = relationKeys.get(targetB.name);
300
- const aProp = deriveBelongsToManyName(targetB.name);
301
- const bProp = deriveBelongsToManyName(targetA.name);
283
+ const aProp = naming.belongsToManyProperty(targetB.name);
284
+ const bProp = naming.belongsToManyProperty(targetA.name);
302
285
  if (!aKey.has(aProp)) {
303
286
  aKey.add(aProp);
304
287
  relationMap.get(targetA.name)?.push({
@@ -329,10 +312,13 @@ const mapRelations = tables => {
329
312
  const fkCols = fkIndex.get(table.name) || [];
330
313
  for (const fk of fkCols) {
331
314
  const targetTable = fk.references.table;
315
+ const targetKey = normalizeName(targetTable);
332
316
  const belongsKey = relationKeys.get(table.name);
333
- const hasManyKey = relationKeys.get(targetTable);
317
+ const hasManyKey = targetKey ? relationKeys.get(targetKey) : undefined;
334
318
 
335
- const belongsProp = deriveBelongsToName(fk.name, targetTable);
319
+ if (!belongsKey || !hasManyKey) continue;
320
+
321
+ const belongsProp = naming.belongsToProperty(fk.name, targetTable);
336
322
  if (!belongsKey.has(belongsProp)) {
337
323
  belongsKey.add(belongsProp);
338
324
  relationMap.get(table.name)?.push({
@@ -343,10 +329,10 @@ const mapRelations = tables => {
343
329
  });
344
330
  }
345
331
 
346
- const hasManyProp = deriveHasManyName(table.name);
332
+ const hasManyProp = naming.hasManyProperty(table.name);
347
333
  if (!hasManyKey.has(hasManyProp)) {
348
334
  hasManyKey.add(hasManyProp);
349
- relationMap.get(targetTable)?.push({
335
+ relationMap.get(targetKey)?.push({
350
336
  kind: 'hasMany',
351
337
  property: hasManyProp,
352
338
  target: table.name,
@@ -360,16 +346,37 @@ const mapRelations = tables => {
360
346
  };
361
347
 
362
348
  const renderEntityFile = (schema, options) => {
349
+ const naming = options.naming || createNamingStrategy('en');
363
350
  const tables = schema.tables.map(t => ({
364
351
  name: t.name,
352
+ schema: t.schema,
365
353
  columns: t.columns,
366
354
  primaryKey: t.primaryKey || []
367
355
  }));
368
356
 
369
357
  const classNames = new Map();
370
- tables.forEach(t => classNames.set(t.name, deriveClassName(t.name)));
358
+ tables.forEach(t => {
359
+ const className = naming.classNameFromTable(t.name);
360
+ classNames.set(t.name, className);
361
+ if (t.schema) {
362
+ const qualified = `${t.schema}.${t.name}`;
363
+ if (!classNames.has(qualified)) {
364
+ classNames.set(qualified, className);
365
+ }
366
+ }
367
+ });
368
+
369
+ const resolveClassName = target => {
370
+ if (!target) return undefined;
371
+ if (classNames.has(target)) return classNames.get(target);
372
+ const fallback = target.split('.').pop();
373
+ if (fallback && classNames.has(fallback)) {
374
+ return classNames.get(fallback);
375
+ }
376
+ return undefined;
377
+ };
371
378
 
372
- const relations = mapRelations(tables);
379
+ const relations = mapRelations(tables, naming);
373
380
 
374
381
  const usage = {
375
382
  needsCol: false,
@@ -454,7 +461,7 @@ const renderEntityFile = (schema, options) => {
454
461
 
455
462
  for (const table of tables) {
456
463
  const className = classNames.get(table.name);
457
- const derivedDefault = deriveDefaultTableNameFromClass(className);
464
+ const derivedDefault = naming.defaultTableNameFromClass(className);
458
465
  const needsTableNameOption = table.name !== derivedDefault;
459
466
  const entityOpts = needsTableNameOption ? `{ tableName: '${escapeJsString(table.name)}' }` : '';
460
467
  lines.push(`@Entity(${entityOpts})`);
@@ -469,7 +476,7 @@ const renderEntityFile = (schema, options) => {
469
476
 
470
477
  const rels = relations.get(table.name) || [];
471
478
  for (const rel of rels) {
472
- const targetClass = classNames.get(rel.target);
479
+ const targetClass = resolveClassName(rel.target);
473
480
  if (!targetClass) continue;
474
481
  switch (rel.kind) {
475
482
  case 'belongsTo':
@@ -487,10 +494,10 @@ const renderEntityFile = (schema, options) => {
487
494
  lines.push('');
488
495
  break;
489
496
  case 'belongsToMany':
497
+ const pivotClass = resolveClassName(rel.pivotTable);
498
+ if (!pivotClass) break;
490
499
  lines.push(
491
- ` @BelongsToMany({ target: () => ${targetClass}, pivotTable: () => ${classNames.get(
492
- rel.pivotTable
493
- )}, pivotForeignKeyToRoot: '${escapeJsString(rel.pivotForeignKeyToRoot)}', pivotForeignKeyToTarget: '${escapeJsString(rel.pivotForeignKeyToTarget)}' })`
500
+ ` @BelongsToMany({ target: () => ${targetClass}, pivotTable: () => ${pivotClass}, pivotForeignKeyToRoot: '${escapeJsString(rel.pivotForeignKeyToRoot)}', pivotForeignKeyToTarget: '${escapeJsString(rel.pivotForeignKeyToTarget)}' })`
494
501
  );
495
502
  lines.push(` ${rel.property}!: ManyToManyCollection<${targetClass}>;`);
496
503
  lines.push('');
@@ -708,6 +715,8 @@ const loadDriver = async (dialect, url, dbPath) => {
708
715
 
709
716
  const main = async () => {
710
717
  const opts = parseArgs();
718
+ const irregulars = opts.namingOverrides ? await loadIrregulars(opts.namingOverrides) : undefined;
719
+ const naming = createNamingStrategy(opts.locale, irregulars);
711
720
 
712
721
  const { executor, cleanup } = await loadDriver(opts.dialect, opts.url, opts.dbPath);
713
722
  let schema;
@@ -721,7 +730,7 @@ const main = async () => {
721
730
  await cleanup?.();
722
731
  }
723
732
 
724
- const code = renderEntityFile(schema, opts);
733
+ const code = renderEntityFile(schema, { ...opts, naming });
725
734
 
726
735
  if (opts.dryRun) {
727
736
  console.log(code);
@@ -0,0 +1,148 @@
1
+ export class BaseNamingStrategy {
2
+ constructor(irregulars = {}) {
3
+ this.irregulars = new Map();
4
+ this.inverseIrregulars = new Map();
5
+ for (const [singular, plural] of Object.entries(irregulars)) {
6
+ if (!singular || !plural) continue;
7
+ const singularKey = singular.toLowerCase();
8
+ const pluralValue = plural.toLowerCase();
9
+ this.irregulars.set(singularKey, pluralValue);
10
+ this.inverseIrregulars.set(pluralValue, singularKey);
11
+ }
12
+ }
13
+
14
+ applyIrregular(word, direction) {
15
+ const lower = word.toLowerCase();
16
+ if (direction === 'plural' && this.irregulars.has(lower)) {
17
+ return this.irregulars.get(lower);
18
+ }
19
+ if (direction === 'singular' && this.inverseIrregulars.has(lower)) {
20
+ return this.inverseIrregulars.get(lower);
21
+ }
22
+ return undefined;
23
+ }
24
+
25
+ toPascalCase(value) {
26
+ return (
27
+ value
28
+ .split(/[^a-zA-Z0-9]+/)
29
+ .filter(Boolean)
30
+ .map(part => part.charAt(0).toUpperCase() + part.slice(1))
31
+ .join('') || 'Entity'
32
+ );
33
+ }
34
+
35
+ toCamelCase(value) {
36
+ const pascal = this.toPascalCase(value);
37
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
38
+ }
39
+
40
+ toSnakeCase(value) {
41
+ return value
42
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
43
+ .replace(/[^a-z0-9_]+/gi, '_')
44
+ .replace(/__+/g, '_')
45
+ .replace(/^_|_$/g, '')
46
+ .toLowerCase();
47
+ }
48
+
49
+ pluralize(word) {
50
+ const irregular = this.applyIrregular(word, 'plural');
51
+ 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`;
56
+ }
57
+
58
+ singularize(word) {
59
+ const irregular = this.applyIrregular(word, 'singular');
60
+ 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;
66
+ }
67
+
68
+ classNameFromTable(tableName) {
69
+ return this.toPascalCase(this.singularize(tableName));
70
+ }
71
+
72
+ belongsToProperty(foreignKeyName, targetTable) {
73
+ const trimmed = foreignKeyName.replace(/_?id$/i, '');
74
+ const base = trimmed && trimmed !== foreignKeyName ? trimmed : this.singularize(targetTable);
75
+ return this.toCamelCase(base);
76
+ }
77
+
78
+ hasManyProperty(targetTable) {
79
+ return this.toCamelCase(this.pluralize(targetTable));
80
+ }
81
+
82
+ belongsToManyProperty(targetTable) {
83
+ return this.toCamelCase(this.pluralize(targetTable));
84
+ }
85
+
86
+ defaultTableNameFromClass(className) {
87
+ const normalized = this.toSnakeCase(className);
88
+ if (!normalized) return 'unknown';
89
+ return this.pluralize(normalized);
90
+ }
91
+ }
92
+
93
+ export class EnglishNamingStrategy extends BaseNamingStrategy {}
94
+
95
+ const DEFAULT_PT_IRREGULARS = {
96
+ mao: 'maos',
97
+ pao: 'paes',
98
+ cao: 'caes',
99
+ mal: 'males',
100
+ consul: 'consules'
101
+ };
102
+
103
+ export class PortugueseNamingStrategy extends BaseNamingStrategy {
104
+ constructor(irregulars = {}) {
105
+ super({ ...DEFAULT_PT_IRREGULARS, ...irregulars });
106
+ }
107
+
108
+ pluralize(word) {
109
+ const irregular = this.applyIrregular(word, 'plural');
110
+ if (irregular) return irregular;
111
+ const lower = word.toLowerCase();
112
+ if (lower.endsWith('cao')) return `${lower.slice(0, -3)}coes`;
113
+ if (lower.endsWith('ao')) return `${lower.slice(0, -2)}oes`;
114
+ if (lower.endsWith('m')) return `${lower.slice(0, -1)}ns`;
115
+ if (lower.endsWith('al')) return `${lower.slice(0, -2)}ais`;
116
+ if (lower.endsWith('el')) return `${lower.slice(0, -2)}eis`;
117
+ if (lower.endsWith('ol')) return `${lower.slice(0, -2)}ois`;
118
+ if (lower.endsWith('ul')) return `${lower.slice(0, -2)}uis`;
119
+ if (lower.endsWith('il')) return `${lower.slice(0, -2)}is`;
120
+ if (/[rznsx]$/.test(lower)) return `${lower}es`;
121
+ if (lower.endsWith('s')) return lower;
122
+ return `${lower}s`;
123
+ }
124
+
125
+ singularize(word) {
126
+ const irregular = this.applyIrregular(word, 'singular');
127
+ if (irregular) return irregular;
128
+ const lower = word.toLowerCase();
129
+ if (lower.endsWith('coes')) return `${lower.slice(0, -4)}cao`;
130
+ if (lower.endsWith('oes')) return `${lower.slice(0, -3)}ao`;
131
+ if (lower.endsWith('ns')) return `${lower.slice(0, -2)}m`;
132
+ if (lower.endsWith('ais')) return `${lower.slice(0, -3)}al`;
133
+ if (lower.endsWith('eis')) return `${lower.slice(0, -3)}el`;
134
+ if (lower.endsWith('ois')) return `${lower.slice(0, -3)}ol`;
135
+ if (lower.endsWith('uis')) return `${lower.slice(0, -3)}ul`;
136
+ if (lower.endsWith('is')) return `${lower.slice(0, -2)}il`;
137
+ if (/[rznsx]es$/.test(lower)) return lower.replace(/es$/, '');
138
+ if (lower.endsWith('s')) return lower.slice(0, -1);
139
+ return lower;
140
+ }
141
+ }
142
+
143
+ export const createNamingStrategy = (locale = 'en', irregulars) => {
144
+ const normalized = (locale || 'en').toLowerCase();
145
+ if (normalized.startsWith('pt')) return new PortugueseNamingStrategy(irregulars);
146
+ if (normalized.startsWith('en')) return new EnglishNamingStrategy(irregulars);
147
+ return new EnglishNamingStrategy(irregulars);
148
+ };
@@ -17,6 +17,7 @@ import {
17
17
  LiteralNode,
18
18
  FunctionNode,
19
19
  AliasRefNode,
20
+ CastExpressionNode,
20
21
  ExpressionVisitor,
21
22
  OperandVisitor,
22
23
  visitExpression,
@@ -41,7 +42,8 @@ type SelectionColumn =
41
42
  | FunctionNode
42
43
  | ScalarSubqueryNode
43
44
  | CaseExpressionNode
44
- | WindowFunctionNode;
45
+ | WindowFunctionNode
46
+ | CastExpressionNode;
45
47
 
46
48
  export class TypeScriptGenerator implements ExpressionVisitor<string>, OperandVisitor<string> {
47
49
  constructor(private namingStrategy: NamingStrategy = new DefaultNamingStrategy()) { }
@@ -181,15 +183,16 @@ export class TypeScriptGenerator implements ExpressionVisitor<string>, OperandVi
181
183
  return `${this.namingStrategy.tableToSymbol(term.table)}.${term.name}`;
182
184
  case 'AliasRef':
183
185
  return this.visitAliasRef(term);
184
- case 'Literal':
185
- case 'Function':
186
- case 'JsonPath':
187
- case 'ScalarSubquery':
188
- case 'CaseExpression':
189
- case 'WindowFunction':
190
- return this.printOperand(term);
191
- default:
192
- return this.printExpression(term);
186
+ case 'Literal':
187
+ case 'Function':
188
+ case 'JsonPath':
189
+ case 'ScalarSubquery':
190
+ case 'CaseExpression':
191
+ case 'WindowFunction':
192
+ case 'Cast':
193
+ return this.printOperand(term);
194
+ default:
195
+ return this.printExpression(term);
193
196
  }
194
197
  }
195
198
 
@@ -263,6 +266,10 @@ export class TypeScriptGenerator implements ExpressionVisitor<string>, OperandVi
263
266
  return this.printWindowFunctionOperand(node);
264
267
  }
265
268
 
269
+ public visitCast(node: CastExpressionNode): string {
270
+ return this.printCastOperand(node);
271
+ }
272
+
266
273
  public visitAliasRef(node: AliasRefNode): string {
267
274
  return `aliasRef('${node.name}')`;
268
275
  }
@@ -454,6 +461,11 @@ export class TypeScriptGenerator implements ExpressionVisitor<string>, OperandVi
454
461
  return result;
455
462
  }
456
463
 
464
+ private printCastOperand(node: CastExpressionNode): string {
465
+ const typeLiteral = node.castType.replace(/'/g, "\\'");
466
+ return `cast(${this.printOperand(node.expression)}, '${typeLiteral}')`;
467
+ }
468
+
457
469
  /**
458
470
  * Converts method chain lines to inline format
459
471
  * @param lines - Method chain lines
@@ -7,6 +7,7 @@ import {
7
7
  JsonPathNode,
8
8
  OperandNode,
9
9
  CaseExpressionNode,
10
+ CastExpressionNode,
10
11
  BinaryExpressionNode,
11
12
  ExpressionNode,
12
13
  LogicalExpressionNode,
@@ -407,6 +408,18 @@ export const caseWhen = (
407
408
  else: elseValue !== undefined ? toOperand(elseValue) : undefined
408
409
  });
409
410
 
411
+ /**
412
+ * Builds a CAST expression node for casting values to SQL types.
413
+ */
414
+ export const cast = (
415
+ expression: OperandNode | ColumnRef | string | number | boolean | null,
416
+ castType: string
417
+ ): CastExpressionNode => ({
418
+ type: 'Cast',
419
+ expression: toOperand(expression),
420
+ castType
421
+ });
422
+
410
423
  /**
411
424
  * Creates an EXISTS expression
412
425
  * @param subquery - Subquery to check for existence
@@ -95,6 +95,19 @@ export interface CaseExpressionNode {
95
95
  alias?: string;
96
96
  }
97
97
 
98
+ /**
99
+ * AST node representing a CAST expression (CAST(value AS type)).
100
+ */
101
+ export interface CastExpressionNode {
102
+ type: 'Cast';
103
+ /** Expression being cast */
104
+ expression: OperandNode;
105
+ /** SQL type literal, e.g. "varchar(255)" */
106
+ castType: string;
107
+ /** Optional alias for the result */
108
+ alias?: string;
109
+ }
110
+
98
111
  /**
99
112
  * AST node representing a window function
100
113
  */
@@ -133,7 +146,9 @@ export type OperandNode =
133
146
  | JsonPathNode
134
147
  | ScalarSubqueryNode
135
148
  | CaseExpressionNode
136
- | WindowFunctionNode;
149
+ | CastExpressionNode
150
+ | WindowFunctionNode
151
+ | ArithmeticExpressionNode;
137
152
 
138
153
  const operandTypes = new Set<OperandNode['type']>([
139
154
  'AliasRef',
@@ -143,7 +158,9 @@ const operandTypes = new Set<OperandNode['type']>([
143
158
  'JsonPath',
144
159
  'ScalarSubquery',
145
160
  'CaseExpression',
146
- 'WindowFunction'
161
+ 'Cast',
162
+ 'WindowFunction',
163
+ 'ArithmeticExpression'
147
164
  ]);
148
165
 
149
166
  const hasTypeProperty = (value: unknown): value is { type?: string } =>
@@ -158,12 +175,15 @@ export const isFunctionNode = (node: unknown): node is FunctionNode =>
158
175
  isOperandNode(node) && node.type === 'Function';
159
176
  export const isCaseExpressionNode = (node: unknown): node is CaseExpressionNode =>
160
177
  isOperandNode(node) && node.type === 'CaseExpression';
178
+
179
+ export const isCastExpressionNode = (node: unknown): node is CastExpressionNode =>
180
+ isOperandNode(node) && node.type === 'Cast';
161
181
  export const isWindowFunctionNode = (node: unknown): node is WindowFunctionNode =>
162
182
  isOperandNode(node) && node.type === 'WindowFunction';
163
183
  export const isExpressionSelectionNode = (
164
- node: ColumnRef | FunctionNode | CaseExpressionNode | WindowFunctionNode
165
- ): node is FunctionNode | CaseExpressionNode | WindowFunctionNode =>
166
- isFunctionNode(node) || isCaseExpressionNode(node) || isWindowFunctionNode(node);
184
+ node: ColumnRef | FunctionNode | CaseExpressionNode | CastExpressionNode | WindowFunctionNode
185
+ ): node is FunctionNode | CaseExpressionNode | CastExpressionNode | WindowFunctionNode =>
186
+ isFunctionNode(node) || isCaseExpressionNode(node) || isCastExpressionNode(node) || isWindowFunctionNode(node);
167
187
 
168
188
  /**
169
189
  * AST node representing a binary expression (e.g., column = value)
@@ -14,6 +14,7 @@ import {
14
14
  JsonPathNode,
15
15
  ScalarSubqueryNode,
16
16
  CaseExpressionNode,
17
+ CastExpressionNode,
17
18
  WindowFunctionNode,
18
19
  AliasRefNode
19
20
  } from './expression-nodes.js';
@@ -42,6 +43,7 @@ export interface OperandVisitor<R> {
42
43
  visitJsonPath?(node: JsonPathNode): R;
43
44
  visitScalarSubquery?(node: ScalarSubqueryNode): R;
44
45
  visitCaseExpression?(node: CaseExpressionNode): R;
46
+ visitCast?(node: CastExpressionNode): R;
45
47
  visitWindowFunction?(node: WindowFunctionNode): R;
46
48
  visitAliasRef?(node: AliasRefNode): R;
47
49
  otherwise?(node: OperandNode): R;
@@ -196,6 +198,9 @@ export const visitOperand = <R>(node: OperandNode, visitor: OperandVisitor<R>):
196
198
  case 'AliasRef':
197
199
  if (visitor.visitAliasRef) return visitor.visitAliasRef(node);
198
200
  break;
201
+ case 'Cast':
202
+ if (visitor.visitCast) return visitor.visitCast(node);
203
+ break;
199
204
  default:
200
205
  break;
201
206
  }
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  AliasRefNode,
3
3
  CaseExpressionNode,
4
+ CastExpressionNode,
4
5
  ColumnNode,
5
6
  ExpressionNode,
6
7
  FunctionNode,
@@ -121,7 +122,14 @@ export interface SelectQueryNode {
121
122
  /** FROM clause table (either a Table or a FunctionTable) */
122
123
  from: TableSourceNode;
123
124
  /** SELECT clause columns */
124
- columns: (ColumnNode | FunctionNode | ScalarSubqueryNode | CaseExpressionNode | WindowFunctionNode)[];
125
+ columns: (
126
+ ColumnNode |
127
+ FunctionNode |
128
+ ScalarSubqueryNode |
129
+ CaseExpressionNode |
130
+ CastExpressionNode |
131
+ WindowFunctionNode
132
+ )[];
125
133
  /** JOIN clauses */
126
134
  joins: JoinNode[];
127
135
  /** Optional WHERE clause */
@@ -1,2 +1,5 @@
1
- /** Re-exports for PostgreSQL catalog definitions. */
1
+ /** Re-exports for supported catalog definitions. */
2
2
  export * from './postgres.js';
3
+ export * from './mssql.js';
4
+ export * from './mysql.js';
5
+ export * from './sqlite.js';