@mikro-orm/sql
Advanced tools
@@ -54,3 +54,3 @@ import { DeferMode, EnumType, Type, Utils, } from '@mikro-orm/core'; | ||
| getListMaterializedViewsSQL() { | ||
| return (`select matviewname as view_name, schemaname as schema_name, definition as view_definition ` + | ||
| return (`select matviewname as view_name, schemaname as schema_name, definition as view_definition, ispopulated as is_populated ` + | ||
| `from pg_matviews ` + | ||
@@ -62,6 +62,15 @@ `where ${this.getIgnoredNamespacesConditionSQL('schemaname')} ` + | ||
| const views = await connection.execute(this.getListMaterializedViewsSQL()); | ||
| for (const view of views) { | ||
| const definition = view.view_definition?.trim().replace(/;$/, '') ?? ''; | ||
| if (views.length === 0) { | ||
| return; | ||
| } | ||
| const tables = views.map(v => ({ table_name: v.view_name, schema_name: v.schema_name })); | ||
| const indexes = await this.getAllIndexes(connection, tables); | ||
| for (let i = 0; i < views.length; i++) { | ||
| const definition = views[i].view_definition?.trim().replace(/;$/, '') ?? ''; | ||
| if (definition) { | ||
| schema.addView(view.view_name, view.schema_name, definition, true); | ||
| const dbView = schema.addView(views[i].view_name, views[i].schema_name, definition, true, views[i].is_populated); | ||
| const key = this.getTableKey(tables[i]); | ||
| if (indexes[key]?.length) { | ||
| dbView.indexes = indexes[key]; | ||
| } | ||
| } | ||
@@ -68,0 +77,0 @@ } |
+2
-2
| { | ||
| "name": "@mikro-orm/sql", | ||
| "version": "7.0.7-dev.10", | ||
| "version": "7.0.7-dev.11", | ||
| "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.", | ||
@@ -56,3 +56,3 @@ "keywords": [ | ||
| "peerDependencies": { | ||
| "@mikro-orm/core": "7.0.7-dev.10" | ||
| "@mikro-orm/core": "7.0.7-dev.11" | ||
| }, | ||
@@ -59,0 +59,0 @@ "engines": { |
@@ -150,3 +150,23 @@ import { ReferenceKind, isRaw, } from '@mikro-orm/core'; | ||
| if (viewDefinition) { | ||
| schema.addView(meta.collection, this.getSchemaName(meta, config, schemaName), viewDefinition, meta.materialized, meta.withData); | ||
| const view = schema.addView(meta.collection, this.getSchemaName(meta, config, schemaName), viewDefinition, meta.materialized, meta.withData); | ||
| if (meta.materialized) { | ||
| // Use a DatabaseTable to resolve property names → field names for indexes. | ||
| // addIndex only needs meta + table name, not actual columns. | ||
| const indexTable = new DatabaseTable(platform, meta.collection, this.getSchemaName(meta, config, schemaName)); | ||
| meta.indexes.forEach(index => indexTable.addIndex(meta, index, 'index')); | ||
| meta.uniques.forEach(index => indexTable.addIndex(meta, index, 'unique')); | ||
| const pkProps = meta.props.filter(prop => prop.primary); | ||
| indexTable.addIndex(meta, { properties: pkProps.map(prop => prop.name) }, 'primary'); | ||
| // Materialized views don't have primary keys or constraints in the DB, | ||
| // convert to match what PostgreSQL stores. | ||
| view.indexes = indexTable.getIndexes().map(idx => { | ||
| if (idx.primary) { | ||
| return { ...idx, primary: false, unique: true, constraint: false }; | ||
| } | ||
| if (idx.constraint) { | ||
| return { ...idx, constraint: false }; | ||
| } | ||
| return idx; | ||
| }); | ||
| } | ||
| } | ||
@@ -153,0 +173,0 @@ continue; |
| import { type Dictionary } from '@mikro-orm/core'; | ||
| import type { Column, ForeignKey, IndexDef, SchemaDifference, TableDifference } from '../typings.js'; | ||
| import type { DatabaseSchema } from './DatabaseSchema.js'; | ||
| import type { DatabaseTable } from './DatabaseTable.js'; | ||
| import { DatabaseTable } from './DatabaseTable.js'; | ||
| import type { AbstractSqlPlatform } from '../AbstractSqlPlatform.js'; | ||
@@ -6,0 +6,0 @@ /** |
| import { ArrayType, BooleanType, DateTimeType, inspect, JsonType, parseJsonSafe, Utils, } from '@mikro-orm/core'; | ||
| import { DatabaseTable } from './DatabaseTable.js'; | ||
| /** | ||
@@ -117,6 +118,7 @@ * Compares two Schemas and return an instance of SchemaDifference. | ||
| } | ||
| // Compare views | ||
| // Compare views — prefer schema-qualified lookup to avoid matching | ||
| // views with the same name in different schemas | ||
| for (const toView of toSchema.getViews()) { | ||
| const viewName = toView.schema ? `${toView.schema}.${toView.name}` : toView.name; | ||
| if (!fromSchema.hasView(toView.name) && !fromSchema.hasView(viewName)) { | ||
| if (!fromSchema.hasView(viewName) && !fromSchema.hasView(toView.name)) { | ||
| diff.newViews[viewName] = toView; | ||
@@ -126,3 +128,3 @@ this.log(`view ${viewName} added`); | ||
| else { | ||
| const fromView = fromSchema.getView(toView.name) ?? fromSchema.getView(viewName); | ||
| const fromView = fromSchema.getView(viewName) ?? fromSchema.getView(toView.name); | ||
| if (fromView && this.diffViewExpression(fromView.definition, toView.definition)) { | ||
@@ -137,3 +139,3 @@ diff.changedViews[viewName] = { from: fromView, to: toView }; | ||
| const viewName = fromView.schema ? `${fromView.schema}.${fromView.name}` : fromView.name; | ||
| if (!toSchema.hasView(fromView.name) && !toSchema.hasView(viewName)) { | ||
| if (!toSchema.hasView(viewName) && !toSchema.hasView(fromView.name)) { | ||
| diff.removedViews[viewName] = fromView; | ||
@@ -143,2 +145,25 @@ this.log(`view ${viewName} removed`); | ||
| } | ||
| // Diff materialized view indexes using the existing table diff infrastructure. | ||
| // Build transient DatabaseTable objects from view indexes so diffTable handles | ||
| // added/removed/changed/renamed index detection and alterTable emits correct DDL. | ||
| for (const toView of toSchema.getViews()) { | ||
| if (!toView.materialized || toView.withData === false) { | ||
| continue; | ||
| } | ||
| const viewName = toView.schema ? `${toView.schema}.${toView.name}` : toView.name; | ||
| // New or definition-changed views have indexes handled during create/recreate | ||
| if (diff.newViews[viewName] || diff.changedViews[viewName]) { | ||
| continue; | ||
| } | ||
| // If we get here, the view exists in fromSchema (otherwise it would be in diff.newViews) | ||
| const fromView = fromSchema.getView(viewName) ?? fromSchema.getView(toView.name); | ||
| const fromTable = new DatabaseTable(this.#platform, fromView.name, fromView.schema); | ||
| fromTable.init([], fromView.indexes ?? [], [], []); | ||
| const toTable = new DatabaseTable(this.#platform, toView.name, toView.schema); | ||
| toTable.init([], toView.indexes ?? [], [], []); | ||
| const tableDiff = this.diffTable(fromTable, toTable); | ||
| if (tableDiff) { | ||
| diff.changedTables[viewName] = tableDiff; | ||
| } | ||
| } | ||
| return diff; | ||
@@ -145,0 +170,0 @@ } |
@@ -87,3 +87,4 @@ import { type Connection, type Dictionary, type Options, RawQueryFragment } from '@mikro-orm/core'; | ||
| createCheck(table: DatabaseTable, check: CheckDef): string; | ||
| protected getTableName(table: string, schema?: string): string; | ||
| /** @internal */ | ||
| getTableName(table: string, schema?: string): string; | ||
| getTablesGroupedBySchemas(tables: Table[]): Map<string | undefined, Table[]>; | ||
@@ -90,0 +91,0 @@ get options(): NonNullable<Options['schemaGenerator']>; |
@@ -596,2 +596,3 @@ import { RawQueryFragment, Utils } from '@mikro-orm/core'; | ||
| } | ||
| /** @internal */ | ||
| getTableName(table, schema) { | ||
@@ -598,0 +599,0 @@ if (schema && schema !== this.platform.getDefaultSchemaName()) { |
@@ -65,4 +65,5 @@ import { type ClearDatabaseOptions, type CreateSchemaOptions, type DropSchemaOptions, type EnsureDatabaseOptions, type EntityMetadata, type ISchemaGenerator, type MikroORM, type Options, type Transaction, type UpdateSchemaOptions } from '@mikro-orm/core'; | ||
| private sortViewsByDependencies; | ||
| private appendViewCreation; | ||
| private escapeRegExp; | ||
| } | ||
| export { SqlSchemaGenerator as SchemaGenerator }; |
@@ -92,12 +92,5 @@ import { CommitOrderCalculator, TableNotFoundException, Utils, } from '@mikro-orm/core'; | ||
| } | ||
| // Create views after tables (views may depend on tables) | ||
| // Sort views by dependencies (views depending on other views come later) | ||
| const sortedViews = this.sortViewsByDependencies(toSchema.getViews()); | ||
| for (const view of sortedViews) { | ||
| if (view.materialized) { | ||
| this.append(ret, this.helper.createMaterializedView(view.name, view.schema, view.definition, view.withData ?? true)); | ||
| } | ||
| else { | ||
| this.append(ret, this.helper.createView(view.name, view.schema, view.definition), true); | ||
| } | ||
| this.appendViewCreation(ret, view); | ||
| } | ||
@@ -342,23 +335,10 @@ return this.wrapSchema(ret, options); | ||
| } | ||
| // Create new views after all table changes are done | ||
| // Sort views by dependencies (views depending on other views come later) | ||
| const sortedNewViews = this.sortViewsByDependencies(Object.values(schemaDiff.newViews)); | ||
| for (const view of sortedNewViews) { | ||
| if (view.materialized) { | ||
| this.append(ret, this.helper.createMaterializedView(view.name, view.schema, view.definition, view.withData ?? true)); | ||
| } | ||
| else { | ||
| this.append(ret, this.helper.createView(view.name, view.schema, view.definition), true); | ||
| } | ||
| this.appendViewCreation(ret, view); | ||
| } | ||
| // Recreate changed views (also sorted by dependencies) | ||
| const changedViews = Object.values(schemaDiff.changedViews).map(v => v.to); | ||
| const sortedChangedViews = this.sortViewsByDependencies(changedViews); | ||
| for (const view of sortedChangedViews) { | ||
| if (view.materialized) { | ||
| this.append(ret, this.helper.createMaterializedView(view.name, view.schema, view.definition, view.withData ?? true)); | ||
| } | ||
| else { | ||
| this.append(ret, this.helper.createView(view.name, view.schema, view.definition), true); | ||
| } | ||
| this.appendViewCreation(ret, view); | ||
| } | ||
@@ -516,2 +496,18 @@ return this.wrapSchema(ret, options); | ||
| } | ||
| appendViewCreation(ret, view) { | ||
| if (view.materialized) { | ||
| this.append(ret, this.helper.createMaterializedView(view.name, view.schema, view.definition, view.withData ?? true)); | ||
| // Skip indexes for WITH NO DATA views — they have no data to index yet. | ||
| // Indexes will be created on the next schema:update after REFRESH populates data. | ||
| if (view.withData !== false) { | ||
| const viewName = this.helper.getTableName(view.name, view.schema); | ||
| for (const index of view.indexes ?? []) { | ||
| this.append(ret, this.helper.getCreateIndexSQL(viewName, index)); | ||
| } | ||
| } | ||
| } | ||
| else { | ||
| this.append(ret, this.helper.createView(view.name, view.schema, view.definition), true); | ||
| } | ||
| } | ||
| escapeRegExp(string) { | ||
@@ -518,0 +514,0 @@ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); |
+2
-0
@@ -145,2 +145,4 @@ import type { Generated, Kysely } from 'kysely'; | ||
| withData?: boolean; | ||
| /** Indexes on the materialized view. Only materialized views support indexes. */ | ||
| indexes?: IndexDef[]; | ||
| } | ||
@@ -147,0 +149,0 @@ export interface SchemaDifference { |
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
857851
0.39%18265
0.3%