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.
@@ -1,16 +1,20 @@
1
1
  import { SchemaIntrospector, IntrospectOptions } from './types.js';
2
- import { queryRows, shouldIncludeTable } from './utils.js';
2
+ import { shouldIncludeTable } from './utils.js';
3
3
  import { DatabaseSchema, DatabaseTable, DatabaseIndex } from '../schema-types.js';
4
- import { ReferentialAction } from '../../../schema/column-types.js';
5
- import { DbExecutor } from '../../execution/db-executor.js';
4
+ import type { IntrospectContext } from './context.js';
5
+ import { runSelectNode } from './run-select.js';
6
+ import type { SelectQueryNode, TableNode } from '../../ast/query.js';
7
+ import type { ColumnNode } from '../../ast/expression-nodes.js';
8
+ import { eq, notLike, and, valueToOperand } from '../../ast/expression-builders.js';
9
+ import { fnTable } from '../../ast/builders.js';
10
+ import type { ReferentialAction } from '../../../schema/column-types.js';
6
11
 
7
- /** Row type for SQLite table list from sqlite_master. */
8
12
  type SqliteTableRow = {
9
13
  name: string;
10
14
  };
11
15
 
12
- /** Row type for SQLite table column information from PRAGMA table_info. */
13
16
  type SqliteTableInfoRow = {
17
+ cid: number;
14
18
  name: string;
15
19
  type: string;
16
20
  notnull: number;
@@ -18,31 +22,28 @@ type SqliteTableInfoRow = {
18
22
  pk: number;
19
23
  };
20
24
 
21
- /** Row type for SQLite foreign key information from PRAGMA foreign_key_list. */
22
25
  type SqliteForeignKeyRow = {
26
+ id: number;
27
+ seq: number;
23
28
  table: string;
24
29
  from: string;
25
30
  to: string;
26
- on_delete: string | null;
27
31
  on_update: string | null;
32
+ on_delete: string | null;
28
33
  };
29
34
 
30
- /** Row type for SQLite index list from PRAGMA index_list. */
31
35
  type SqliteIndexListRow = {
36
+ seq: number;
32
37
  name: string;
33
38
  unique: number;
34
39
  };
35
40
 
36
- /** Row type for SQLite index column information from PRAGMA index_info. */
37
41
  type SqliteIndexInfoRow = {
42
+ seqno: number;
43
+ cid: number;
38
44
  name: string;
39
45
  };
40
46
 
41
- /**
42
- * Converts a SQLite referential action string to a ReferentialAction enum value.
43
- * @param value - The string value from SQLite pragma (e.g., 'CASCADE', 'SET NULL').
44
- * @returns The corresponding ReferentialAction enum value, or undefined if the value is invalid or null.
45
- */
46
47
  const toReferentialAction = (value: string | null | undefined): ReferentialAction | undefined => {
47
48
  if (!value) return undefined;
48
49
  const normalized = value.toUpperCase();
@@ -58,53 +59,101 @@ const toReferentialAction = (value: string | null | undefined): ReferentialActio
58
59
  return undefined;
59
60
  };
60
61
 
61
- /**
62
- * Escapes single quotes in a string for safe inclusion in SQL queries.
63
- * @param name - The string to escape.
64
- * @returns The escaped string with single quotes doubled.
65
- */
66
- const escapeSingleQuotes = (name: string) => name.replace(/'/g, "''");
62
+ const columnNode = (table: string, name: string, alias?: string): ColumnNode => ({
63
+ type: 'Column',
64
+ table,
65
+ name,
66
+ alias
67
+ });
68
+
69
+ const buildPragmaQuery = (
70
+ name: string,
71
+ table: string,
72
+ alias: string,
73
+ columnAliases: string[]
74
+ ): SelectQueryNode => ({
75
+ type: 'SelectQuery',
76
+ from: fnTable(name, [valueToOperand(table)], alias, { columnAliases }),
77
+ columns: columnAliases.map(column => columnNode(alias, column)),
78
+ joins: []
79
+ });
80
+
81
+ const runPragma = async <T>(
82
+ name: string,
83
+ table: string,
84
+ alias: string,
85
+ columnAliases: string[],
86
+ ctx: IntrospectContext
87
+ ): Promise<T[]> => {
88
+ const query = buildPragmaQuery(name, table, alias, columnAliases);
89
+ return (await runSelectNode<T>(query, ctx)) as T[];
90
+ };
67
91
 
68
- /** SQLite schema introspector. */
69
92
  export const sqliteIntrospector: SchemaIntrospector = {
70
- /**
71
- * Introspects the SQLite database schema by querying sqlite_master and various PRAGMAs.
72
- * @param ctx - The database execution context containing the DbExecutor.
73
- * @param options - Options controlling which tables and schemas to include.
74
- * @returns A promise that resolves to the introspected DatabaseSchema.
75
- */
76
- async introspect(ctx: { executor: DbExecutor }, options: IntrospectOptions): Promise<DatabaseSchema> {
93
+ async introspect(ctx: IntrospectContext, options: IntrospectOptions): Promise<DatabaseSchema> {
94
+ const alias = 'sqlite_master';
95
+ const tablesQuery: SelectQueryNode = {
96
+ type: 'SelectQuery',
97
+ from: { type: 'Table', name: 'sqlite_master' } as TableNode,
98
+ columns: [columnNode(alias, 'name')],
99
+ joins: [],
100
+ where: and(
101
+ eq(columnNode(alias, 'type'), 'table'),
102
+ notLike(columnNode(alias, 'name'), 'sqlite_%')
103
+ )
104
+ };
105
+
106
+ const tableRows = (await runSelectNode<SqliteTableRow>(tablesQuery, ctx)) as SqliteTableRow[];
77
107
  const tables: DatabaseTable[] = [];
78
- const tableRows = (await queryRows(
79
- ctx.executor,
80
- `SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%';`
81
- )) as SqliteTableRow[];
82
108
 
83
109
  for (const row of tableRows) {
84
- const name = row.name;
85
- if (!shouldIncludeTable(name, options)) continue;
86
- const table: DatabaseTable = { name, columns: [], primaryKey: [], indexes: [] };
87
-
88
- const cols = (await queryRows(ctx.executor, `PRAGMA table_info('${escapeSingleQuotes(name)}');`)) as SqliteTableInfoRow[];
89
- cols.forEach(c => {
90
- table.columns.push({
91
- name: c.name,
92
- type: c.type,
93
- notNull: c.notnull === 1,
94
- default: c.dflt_value ?? undefined,
110
+ const tableName = row.name;
111
+ if (!shouldIncludeTable(tableName, options)) continue;
112
+
113
+ const tableInfo = await runPragma<SqliteTableInfoRow>(
114
+ 'pragma_table_info',
115
+ tableName,
116
+ 'ti',
117
+ ['cid', 'name', 'type', 'notnull', 'dflt_value', 'pk'],
118
+ ctx
119
+ );
120
+
121
+ const foreignKeys = await runPragma<SqliteForeignKeyRow>(
122
+ 'pragma_foreign_key_list',
123
+ tableName,
124
+ 'fk',
125
+ ['id', 'seq', 'table', 'from', 'to', 'on_update', 'on_delete', 'match'],
126
+ ctx
127
+ );
128
+
129
+ const indexList = await runPragma<SqliteIndexListRow>(
130
+ 'pragma_index_list',
131
+ tableName,
132
+ 'idx',
133
+ ['seq', 'name', 'unique'],
134
+ ctx
135
+ );
136
+
137
+ const tableEntry: DatabaseTable = { name: tableName, columns: [], primaryKey: [], indexes: [] };
138
+
139
+ tableInfo.forEach(info => {
140
+ tableEntry.columns.push({
141
+ name: info.name,
142
+ type: info.type,
143
+ notNull: info.notnull === 1,
144
+ default: info.dflt_value ?? undefined,
95
145
  autoIncrement: false
96
146
  });
97
- if (c.pk && c.pk > 0) {
98
- table.primaryKey = table.primaryKey || [];
99
- table.primaryKey.push(c.name);
147
+ if (info.pk && info.pk > 0) {
148
+ tableEntry.primaryKey = tableEntry.primaryKey || [];
149
+ tableEntry.primaryKey.push(info.name);
100
150
  }
101
151
  });
102
152
 
103
- const fkRows = (await queryRows(ctx.executor, `PRAGMA foreign_key_list('${escapeSingleQuotes(name)}');`)) as SqliteForeignKeyRow[];
104
- fkRows.forEach(fk => {
105
- const col = table.columns.find(c => c.name === fk.from);
106
- if (col) {
107
- col.references = {
153
+ foreignKeys.forEach(fk => {
154
+ const column = tableEntry.columns.find(col => col.name === fk.from);
155
+ if (column) {
156
+ column.references = {
108
157
  table: fk.table,
109
158
  column: fk.to,
110
159
  onDelete: toReferentialAction(fk.on_delete),
@@ -113,22 +162,26 @@ export const sqliteIntrospector: SchemaIntrospector = {
113
162
  }
114
163
  });
115
164
 
116
- const idxList = (await queryRows(ctx.executor, `PRAGMA index_list('${escapeSingleQuotes(name)}');`)) as SqliteIndexListRow[];
117
- for (const idx of idxList) {
118
- const idxName = idx.name;
119
- const columnsInfo = (await queryRows(ctx.executor, `PRAGMA index_info('${escapeSingleQuotes(idxName)}');`)) as SqliteIndexInfoRow[];
165
+ for (const idx of indexList) {
166
+ if (!idx.name) continue;
167
+ const indexColumns = await runPragma<SqliteIndexInfoRow>(
168
+ 'pragma_index_info',
169
+ idx.name,
170
+ 'info',
171
+ ['seqno', 'cid', 'name'],
172
+ ctx
173
+ );
120
174
  const idxEntry: DatabaseIndex = {
121
- name: idxName,
122
- columns: columnsInfo.map(ci => ({ column: ci.name })),
175
+ name: idx.name,
176
+ columns: indexColumns.map(col => ({ column: col.name })),
123
177
  unique: idx.unique === 1
124
178
  };
125
- table.indexes!.push(idxEntry);
179
+ tableEntry.indexes!.push(idxEntry);
126
180
  }
127
181
 
128
- tables.push(table);
182
+ tables.push(tableEntry);
129
183
  }
130
184
 
131
185
  return { tables };
132
186
  }
133
187
  };
134
-
@@ -19,6 +19,7 @@ export interface DatabaseColumn {
19
19
  generated?: 'always' | 'byDefault';
20
20
  unique?: boolean | string;
21
21
  references?: ForeignKeyReference;
22
+ comment?: string;
22
23
  check?: string;
23
24
  }
24
25
 
@@ -44,10 +45,10 @@ export interface DatabaseTable {
44
45
  primaryKey?: string[];
45
46
  indexes?: DatabaseIndex[];
46
47
  checks?: DatabaseCheck[];
48
+ comment?: string;
47
49
  }
48
50
 
49
51
  /** Represents the overall database schema. */
50
52
  export interface DatabaseSchema {
51
53
  tables: DatabaseTable[];
52
54
  }
53
-
@@ -21,10 +21,11 @@ import {
21
21
  JsonPathNode,
22
22
  ScalarSubqueryNode,
23
23
  CaseExpressionNode,
24
+ CastExpressionNode,
24
25
  WindowFunctionNode,
25
26
  BetweenExpressionNode,
26
- AliasRefNode,
27
27
  ArithmeticExpressionNode,
28
+ AliasRefNode,
28
29
  isOperandNode
29
30
  } from '../ast/expression.js';
30
31
  import { DialectName } from '../sql/sql.js';
@@ -476,6 +477,11 @@ export abstract class Dialect
476
477
  return parts.join(' ');
477
478
  });
478
479
 
480
+ this.registerOperandCompiler('Cast', (node: CastExpressionNode, ctx) => {
481
+ const value = this.compileOperand(node.expression, ctx);
482
+ return `CAST(${value} AS ${node.castType})`;
483
+ });
484
+
479
485
  this.registerOperandCompiler('WindowFunction', (node: WindowFunctionNode, ctx) => {
480
486
  let result = `${node.name}(`;
481
487
  if (node.args.length > 0) {
@@ -507,6 +513,11 @@ export abstract class Dialect
507
513
 
508
514
  return result;
509
515
  });
516
+ this.registerOperandCompiler('ArithmeticExpression', (node: ArithmeticExpressionNode, ctx) => {
517
+ const left = this.compileOperand(node.left, ctx);
518
+ const right = this.compileOperand(node.right, ctx);
519
+ return `(${left} ${node.operator} ${right})`;
520
+ });
510
521
  }
511
522
 
512
523
  // Default fallback, should be overridden by dialects if supported
@@ -105,16 +105,10 @@ export class SqlServerDialect extends SqlDialectBase {
105
105
 
106
106
  private compileSelectCoreForMssql(ast: SelectQueryNode, ctx: CompilerContext): string {
107
107
  const columns = ast.columns.map(c => {
108
- let expr = '';
109
- if (c.type === 'Function') {
110
- expr = this.compileOperand(c, ctx);
111
- } else if (c.type === 'Column') {
112
- expr = `${this.quoteIdentifier(c.table)}.${this.quoteIdentifier(c.name)}`;
113
- } else if (c.type === 'ScalarSubquery') {
114
- expr = this.compileOperand(c, ctx);
115
- } else if (c.type === 'WindowFunction') {
116
- expr = this.compileOperand(c, ctx);
117
- }
108
+ // Default to full operand compilation for all projection node types (Function, Column, Cast, Case, Window, etc)
109
+ const expr = c.type === 'Column'
110
+ ? `${this.quoteIdentifier(c.table)}.${this.quoteIdentifier(c.name)}`
111
+ : this.compileOperand(c as unknown as import('../../ast/expression.js').OperandNode, ctx);
118
112
 
119
113
  if (c.alias) {
120
114
  if (c.alias.includes('(')) return c.alias;
@@ -15,6 +15,7 @@ import {
15
15
  ExpressionNode,
16
16
  FunctionNode,
17
17
  CaseExpressionNode,
18
+ CastExpressionNode,
18
19
  WindowFunctionNode,
19
20
  ScalarSubqueryNode,
20
21
  and,
@@ -57,7 +58,7 @@ export class QueryAstService {
57
58
  * @returns Column selection result with updated state and added columns
58
59
  */
59
60
  select(
60
- columns: Record<string, ColumnDef | FunctionNode | CaseExpressionNode | WindowFunctionNode>
61
+ columns: Record<string, ColumnDef | FunctionNode | CaseExpressionNode | CastExpressionNode | WindowFunctionNode>
61
62
  ): ColumnSelectionResult {
62
63
  const existingAliases = new Set(
63
64
  this.state.ast.columns.map(c => (c as ColumnNode).alias || (c as ColumnNode).name)
@@ -69,7 +70,7 @@ export class QueryAstService {
69
70
  if (existingAliases.has(alias)) return acc;
70
71
 
71
72
  if (isExpressionSelectionNode(val)) {
72
- acc.push({ ...(val as FunctionNode | CaseExpressionNode | WindowFunctionNode), alias } as ProjectionNode);
73
+ acc.push({ ...(val as FunctionNode | CaseExpressionNode | CastExpressionNode | WindowFunctionNode), alias } as ProjectionNode);
73
74
  return acc;
74
75
  }
75
76
 
@@ -284,4 +285,3 @@ export class QueryAstService {
284
285
  }
285
286
 
286
287
  }
287
-
@@ -14,6 +14,7 @@ import {
14
14
  FunctionNode,
15
15
  ScalarSubqueryNode,
16
16
  CaseExpressionNode,
17
+ CastExpressionNode,
17
18
  WindowFunctionNode
18
19
  } from '../core/ast/expression.js';
19
20
  import { JoinNode } from '../core/ast/join.js';
@@ -26,6 +27,7 @@ export type ProjectionNode =
26
27
  | FunctionNode
27
28
  | ScalarSubqueryNode
28
29
  | CaseExpressionNode
30
+ | CastExpressionNode
29
31
  | WindowFunctionNode;
30
32
 
31
33
  /**