@mikro-orm/sql 7.0.7-dev.10 → 7.0.7-dev.11

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.
@@ -51,17 +51,26 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
51
51
  }
52
52
  }
53
53
  getListMaterializedViewsSQL() {
54
- return (`select matviewname as view_name, schemaname as schema_name, definition as view_definition ` +
54
+ return (`select matviewname as view_name, schemaname as schema_name, definition as view_definition, ispopulated as is_populated ` +
55
55
  `from pg_matviews ` +
56
56
  `where ${this.getIgnoredNamespacesConditionSQL('schemaname')} ` +
57
57
  `order by matviewname`);
58
58
  }
59
59
  async loadMaterializedViews(schema, connection, schemaName) {
60
60
  const views = await connection.execute(this.getListMaterializedViewsSQL());
61
- for (const view of views) {
62
- const definition = view.view_definition?.trim().replace(/;$/, '') ?? '';
61
+ if (views.length === 0) {
62
+ return;
63
+ }
64
+ const tables = views.map(v => ({ table_name: v.view_name, schema_name: v.schema_name }));
65
+ const indexes = await this.getAllIndexes(connection, tables);
66
+ for (let i = 0; i < views.length; i++) {
67
+ const definition = views[i].view_definition?.trim().replace(/;$/, '') ?? '';
63
68
  if (definition) {
64
- schema.addView(view.view_name, view.schema_name, definition, true);
69
+ const dbView = schema.addView(views[i].view_name, views[i].schema_name, definition, true, views[i].is_populated);
70
+ const key = this.getTableKey(tables[i]);
71
+ if (indexes[key]?.length) {
72
+ dbView.indexes = indexes[key];
73
+ }
65
74
  }
66
75
  }
67
76
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/sql",
3
- "version": "7.0.7-dev.10",
3
+ "version": "7.0.7-dev.11",
4
4
  "description": "TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, PostgreSQL and SQLite databases as well as usage with vanilla JavaScript.",
5
5
  "keywords": [
6
6
  "data-mapper",
@@ -53,7 +53,7 @@
53
53
  "@mikro-orm/core": "^7.0.6"
54
54
  },
55
55
  "peerDependencies": {
56
- "@mikro-orm/core": "7.0.7-dev.10"
56
+ "@mikro-orm/core": "7.0.7-dev.11"
57
57
  },
58
58
  "engines": {
59
59
  "node": ">= 22.17.0"
@@ -147,7 +147,27 @@ export class DatabaseSchema {
147
147
  if (meta.view) {
148
148
  const viewDefinition = this.getViewDefinition(meta, em, platform);
149
149
  if (viewDefinition) {
150
- schema.addView(meta.collection, this.getSchemaName(meta, config, schemaName), viewDefinition, meta.materialized, meta.withData);
150
+ const view = schema.addView(meta.collection, this.getSchemaName(meta, config, schemaName), viewDefinition, meta.materialized, meta.withData);
151
+ if (meta.materialized) {
152
+ // Use a DatabaseTable to resolve property names → field names for indexes.
153
+ // addIndex only needs meta + table name, not actual columns.
154
+ const indexTable = new DatabaseTable(platform, meta.collection, this.getSchemaName(meta, config, schemaName));
155
+ meta.indexes.forEach(index => indexTable.addIndex(meta, index, 'index'));
156
+ meta.uniques.forEach(index => indexTable.addIndex(meta, index, 'unique'));
157
+ const pkProps = meta.props.filter(prop => prop.primary);
158
+ indexTable.addIndex(meta, { properties: pkProps.map(prop => prop.name) }, 'primary');
159
+ // Materialized views don't have primary keys or constraints in the DB,
160
+ // convert to match what PostgreSQL stores.
161
+ view.indexes = indexTable.getIndexes().map(idx => {
162
+ if (idx.primary) {
163
+ return { ...idx, primary: false, unique: true, constraint: false };
164
+ }
165
+ if (idx.constraint) {
166
+ return { ...idx, constraint: false };
167
+ }
168
+ return idx;
169
+ });
170
+ }
151
171
  }
152
172
  continue;
153
173
  }
@@ -1,7 +1,7 @@
1
1
  import { type Dictionary } from '@mikro-orm/core';
2
2
  import type { Column, ForeignKey, IndexDef, SchemaDifference, TableDifference } from '../typings.js';
3
3
  import type { DatabaseSchema } from './DatabaseSchema.js';
4
- import type { DatabaseTable } from './DatabaseTable.js';
4
+ import { DatabaseTable } from './DatabaseTable.js';
5
5
  import type { AbstractSqlPlatform } from '../AbstractSqlPlatform.js';
6
6
  /**
7
7
  * Compares two Schemas and return an instance of SchemaDifference.
@@ -1,4 +1,5 @@
1
1
  import { ArrayType, BooleanType, DateTimeType, inspect, JsonType, parseJsonSafe, Utils, } from '@mikro-orm/core';
2
+ import { DatabaseTable } from './DatabaseTable.js';
2
3
  /**
3
4
  * Compares two Schemas and return an instance of SchemaDifference.
4
5
  */
@@ -114,15 +115,16 @@ export class SchemaComparator {
114
115
  }
115
116
  }
116
117
  }
117
- // Compare views
118
+ // Compare views — prefer schema-qualified lookup to avoid matching
119
+ // views with the same name in different schemas
118
120
  for (const toView of toSchema.getViews()) {
119
121
  const viewName = toView.schema ? `${toView.schema}.${toView.name}` : toView.name;
120
- if (!fromSchema.hasView(toView.name) && !fromSchema.hasView(viewName)) {
122
+ if (!fromSchema.hasView(viewName) && !fromSchema.hasView(toView.name)) {
121
123
  diff.newViews[viewName] = toView;
122
124
  this.log(`view ${viewName} added`);
123
125
  }
124
126
  else {
125
- const fromView = fromSchema.getView(toView.name) ?? fromSchema.getView(viewName);
127
+ const fromView = fromSchema.getView(viewName) ?? fromSchema.getView(toView.name);
126
128
  if (fromView && this.diffViewExpression(fromView.definition, toView.definition)) {
127
129
  diff.changedViews[viewName] = { from: fromView, to: toView };
128
130
  this.log(`view ${viewName} changed`);
@@ -132,11 +134,34 @@ export class SchemaComparator {
132
134
  // Check for removed views
133
135
  for (const fromView of fromSchema.getViews()) {
134
136
  const viewName = fromView.schema ? `${fromView.schema}.${fromView.name}` : fromView.name;
135
- if (!toSchema.hasView(fromView.name) && !toSchema.hasView(viewName)) {
137
+ if (!toSchema.hasView(viewName) && !toSchema.hasView(fromView.name)) {
136
138
  diff.removedViews[viewName] = fromView;
137
139
  this.log(`view ${viewName} removed`);
138
140
  }
139
141
  }
142
+ // Diff materialized view indexes using the existing table diff infrastructure.
143
+ // Build transient DatabaseTable objects from view indexes so diffTable handles
144
+ // added/removed/changed/renamed index detection and alterTable emits correct DDL.
145
+ for (const toView of toSchema.getViews()) {
146
+ if (!toView.materialized || toView.withData === false) {
147
+ continue;
148
+ }
149
+ const viewName = toView.schema ? `${toView.schema}.${toView.name}` : toView.name;
150
+ // New or definition-changed views have indexes handled during create/recreate
151
+ if (diff.newViews[viewName] || diff.changedViews[viewName]) {
152
+ continue;
153
+ }
154
+ // If we get here, the view exists in fromSchema (otherwise it would be in diff.newViews)
155
+ const fromView = fromSchema.getView(viewName) ?? fromSchema.getView(toView.name);
156
+ const fromTable = new DatabaseTable(this.#platform, fromView.name, fromView.schema);
157
+ fromTable.init([], fromView.indexes ?? [], [], []);
158
+ const toTable = new DatabaseTable(this.#platform, toView.name, toView.schema);
159
+ toTable.init([], toView.indexes ?? [], [], []);
160
+ const tableDiff = this.diffTable(fromTable, toTable);
161
+ if (tableDiff) {
162
+ diff.changedTables[viewName] = tableDiff;
163
+ }
164
+ }
140
165
  return diff;
141
166
  }
142
167
  /**
@@ -84,7 +84,8 @@ export declare abstract class SchemaHelper {
84
84
  getReferencedTableName(referencedTableName: string, schema?: string): string;
85
85
  createIndex(index: IndexDef, table: DatabaseTable, createPrimary?: boolean): string;
86
86
  createCheck(table: DatabaseTable, check: CheckDef): string;
87
- protected getTableName(table: string, schema?: string): string;
87
+ /** @internal */
88
+ getTableName(table: string, schema?: string): string;
88
89
  getTablesGroupedBySchemas(tables: Table[]): Map<string | undefined, Table[]>;
89
90
  get options(): NonNullable<Options['schemaGenerator']>;
90
91
  protected processComment(comment: string): string;
@@ -593,6 +593,7 @@ export class SchemaHelper {
593
593
  createCheck(table, check) {
594
594
  return `alter table ${table.getQuotedName()} add constraint ${this.quote(check.name)} check (${check.expression})`;
595
595
  }
596
+ /** @internal */
596
597
  getTableName(table, schema) {
597
598
  if (schema && schema !== this.platform.getDefaultSchemaName()) {
598
599
  return `${schema}.${table}`;
@@ -62,6 +62,7 @@ export declare class SqlSchemaGenerator extends AbstractSchemaGenerator<Abstract
62
62
  * Uses topological sort based on view definition string matching.
63
63
  */
64
64
  private sortViewsByDependencies;
65
+ private appendViewCreation;
65
66
  private escapeRegExp;
66
67
  }
67
68
  export { SqlSchemaGenerator as SchemaGenerator };
@@ -89,16 +89,9 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
89
89
  this.append(ret, fks, true);
90
90
  }
91
91
  }
92
- // Create views after tables (views may depend on tables)
93
- // Sort views by dependencies (views depending on other views come later)
94
92
  const sortedViews = this.sortViewsByDependencies(toSchema.getViews());
95
93
  for (const view of sortedViews) {
96
- if (view.materialized) {
97
- this.append(ret, this.helper.createMaterializedView(view.name, view.schema, view.definition, view.withData ?? true));
98
- }
99
- else {
100
- this.append(ret, this.helper.createView(view.name, view.schema, view.definition), true);
101
- }
94
+ this.appendViewCreation(ret, view);
102
95
  }
103
96
  return this.wrapSchema(ret, options);
104
97
  }
@@ -338,27 +331,14 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
338
331
  this.append(ret, sql);
339
332
  }
340
333
  }
341
- // Create new views after all table changes are done
342
- // Sort views by dependencies (views depending on other views come later)
343
334
  const sortedNewViews = this.sortViewsByDependencies(Object.values(schemaDiff.newViews));
344
335
  for (const view of sortedNewViews) {
345
- if (view.materialized) {
346
- this.append(ret, this.helper.createMaterializedView(view.name, view.schema, view.definition, view.withData ?? true));
347
- }
348
- else {
349
- this.append(ret, this.helper.createView(view.name, view.schema, view.definition), true);
350
- }
336
+ this.appendViewCreation(ret, view);
351
337
  }
352
- // Recreate changed views (also sorted by dependencies)
353
338
  const changedViews = Object.values(schemaDiff.changedViews).map(v => v.to);
354
339
  const sortedChangedViews = this.sortViewsByDependencies(changedViews);
355
340
  for (const view of sortedChangedViews) {
356
- if (view.materialized) {
357
- this.append(ret, this.helper.createMaterializedView(view.name, view.schema, view.definition, view.withData ?? true));
358
- }
359
- else {
360
- this.append(ret, this.helper.createView(view.name, view.schema, view.definition), true);
361
- }
341
+ this.appendViewCreation(ret, view);
362
342
  }
363
343
  return this.wrapSchema(ret, options);
364
344
  }
@@ -511,6 +491,22 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
511
491
  // Sort and map back to views
512
492
  return calc.sort().map(index => indexToView.get(index));
513
493
  }
494
+ appendViewCreation(ret, view) {
495
+ if (view.materialized) {
496
+ this.append(ret, this.helper.createMaterializedView(view.name, view.schema, view.definition, view.withData ?? true));
497
+ // Skip indexes for WITH NO DATA views — they have no data to index yet.
498
+ // Indexes will be created on the next schema:update after REFRESH populates data.
499
+ if (view.withData !== false) {
500
+ const viewName = this.helper.getTableName(view.name, view.schema);
501
+ for (const index of view.indexes ?? []) {
502
+ this.append(ret, this.helper.getCreateIndexSQL(viewName, index));
503
+ }
504
+ }
505
+ }
506
+ else {
507
+ this.append(ret, this.helper.createView(view.name, view.schema, view.definition), true);
508
+ }
509
+ }
514
510
  escapeRegExp(string) {
515
511
  return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
516
512
  }
package/typings.d.ts CHANGED
@@ -142,6 +142,8 @@ export interface DatabaseView {
142
142
  materialized?: boolean;
143
143
  /** For materialized views, whether data was populated on creation. */
144
144
  withData?: boolean;
145
+ /** Indexes on the materialized view. Only materialized views support indexes. */
146
+ indexes?: IndexDef[];
145
147
  }
146
148
  export interface SchemaDifference {
147
149
  newNamespaces: Set<string>;