@contember/engine-system-api 1.3.0-alpha.7 → 1.3.0-alpha.8
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/dist/src/SystemContainer.d.ts +5 -1
- package/dist/src/SystemContainer.d.ts.map +1 -1
- package/dist/src/SystemContainer.js +3 -1
- package/dist/src/SystemContainer.js.map +1 -1
- package/dist/src/bin/runMigrations.js +4 -1
- package/dist/src/bin/runMigrations.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/migrations/2022-10-03-110000-table-on-delete.d.ts.map +1 -1
- package/dist/src/migrations/2022-10-03-110000-table-on-delete.js +17 -12
- package/dist/src/migrations/2022-10-03-110000-table-on-delete.js.map +1 -1
- package/dist/src/migrations/runner.d.ts +3 -1
- package/dist/src/migrations/runner.d.ts.map +1 -1
- package/dist/src/migrations/runner.js +6 -1
- package/dist/src/migrations/runner.js.map +1 -1
- package/dist/src/migrations/types.d.ts +2 -0
- package/dist/src/migrations/types.d.ts.map +1 -1
- package/dist/src/model/index.d.ts +1 -0
- package/dist/src/model/index.d.ts.map +1 -1
- package/dist/src/model/index.js +1 -0
- package/dist/src/model/index.js.map +1 -1
- package/dist/src/model/metadata/ResolvedDatabaseMetadata.d.ts +13 -0
- package/dist/src/model/metadata/ResolvedDatabaseMetadata.d.ts.map +1 -0
- package/dist/src/model/metadata/ResolvedDatabaseMetadata.js +42 -0
- package/dist/src/model/metadata/ResolvedDatabaseMetadata.js.map +1 -0
- package/dist/src/model/metadata/SchemaDatabaseMetadataResolver.d.ts +8 -0
- package/dist/src/model/metadata/SchemaDatabaseMetadataResolver.d.ts.map +1 -0
- package/dist/src/model/metadata/SchemaDatabaseMetadataResolver.js +92 -0
- package/dist/src/model/metadata/SchemaDatabaseMetadataResolver.js.map +1 -0
- package/dist/src/model/metadata/SchemaDatabaseMetadataResolverStore.d.ts +17 -0
- package/dist/src/model/metadata/SchemaDatabaseMetadataResolverStore.d.ts.map +1 -0
- package/dist/src/model/metadata/SchemaDatabaseMetadataResolverStore.js +33 -0
- package/dist/src/model/metadata/SchemaDatabaseMetadataResolverStore.js.map +1 -0
- package/dist/src/model/metadata/index.d.ts +4 -0
- package/dist/src/model/metadata/index.d.ts.map +1 -0
- package/dist/src/model/metadata/index.js +22 -0
- package/dist/src/model/metadata/index.js.map +1 -0
- package/dist/src/model/migrations/ProjectMigrator.d.ts +3 -1
- package/dist/src/model/migrations/ProjectMigrator.d.ts.map +1 -1
- package/dist/src/model/migrations/ProjectMigrator.js +13 -5
- package/dist/src/model/migrations/ProjectMigrator.js.map +1 -1
- package/dist/src/tsconfig.tsbuildinfo +1 -1
- package/dist/tests/cases/migrations/2020-05-06-150000-composed-primary.test.js +2 -0
- package/dist/tests/cases/migrations/2020-05-06-150000-composed-primary.test.js.map +1 -1
- package/dist/tests/cases/migrations/2020-06-01-103000-event-trigger-perf.test.js +2 -0
- package/dist/tests/cases/migrations/2020-06-01-103000-event-trigger-perf.test.js.map +1 -1
- package/dist/tests/cases/migrations/2022-10-03-110000-table-on-delete.test.js +5 -2
- package/dist/tests/cases/migrations/2022-10-03-110000-table-on-delete.test.js.map +1 -1
- package/dist/tests/tsconfig.tsbuildinfo +1 -1
- package/package.json +13 -13
- package/src/SystemContainer.ts +8 -3
- package/src/bin/runMigrations.ts +8 -1
- package/src/index.ts +1 -0
- package/src/migrations/2022-10-03-110000-table-on-delete.ts +18 -19
- package/src/migrations/runner.ts +7 -1
- package/src/migrations/types.ts +2 -0
- package/src/model/index.ts +1 -0
- package/src/model/metadata/ResolvedDatabaseMetadata.ts +54 -0
- package/src/model/metadata/SchemaDatabaseMetadataResolver.ts +136 -0
- package/src/model/metadata/SchemaDatabaseMetadataResolverStore.ts +41 -0
- package/src/model/metadata/index.ts +3 -0
- package/src/model/migrations/ProjectMigrator.ts +18 -4
- package/tests/cases/migrations/2020-05-06-150000-composed-primary.test.ts +2 -0
- package/tests/cases/migrations/2020-06-01-103000-event-trigger-perf.test.ts +2 -0
- package/tests/cases/migrations/2022-10-03-110000-table-on-delete.test.ts +6 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contember/engine-system-api",
|
|
3
|
-
"version": "1.3.0-alpha.
|
|
3
|
+
"version": "1.3.0-alpha.8",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"main": "dist/src/index.js",
|
|
6
6
|
"typings": "dist/src/index.d.ts",
|
|
@@ -13,22 +13,22 @@
|
|
|
13
13
|
"test": "vitest"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@contember/authorization": "^1.3.0-alpha.
|
|
17
|
-
"@contember/database": "^1.3.0-alpha.
|
|
18
|
-
"@contember/database-migrations": "^1.3.0-alpha.
|
|
19
|
-
"@contember/dic": "^1.3.0-alpha.
|
|
20
|
-
"@contember/graphql-utils": "^1.3.0-alpha.
|
|
21
|
-
"@contember/logger": "^1.3.0-alpha.
|
|
22
|
-
"@contember/queryable": "^1.3.0-alpha.
|
|
23
|
-
"@contember/schema": "^1.3.0-alpha.
|
|
24
|
-
"@contember/schema-migrations": "^1.3.0-alpha.
|
|
25
|
-
"@contember/schema-utils": "^1.3.0-alpha.
|
|
16
|
+
"@contember/authorization": "^1.3.0-alpha.8",
|
|
17
|
+
"@contember/database": "^1.3.0-alpha.8",
|
|
18
|
+
"@contember/database-migrations": "^1.3.0-alpha.8",
|
|
19
|
+
"@contember/dic": "^1.3.0-alpha.8",
|
|
20
|
+
"@contember/graphql-utils": "^1.3.0-alpha.8",
|
|
21
|
+
"@contember/logger": "^1.3.0-alpha.8",
|
|
22
|
+
"@contember/queryable": "^1.3.0-alpha.8",
|
|
23
|
+
"@contember/schema": "^1.3.0-alpha.8",
|
|
24
|
+
"@contember/schema-migrations": "^1.3.0-alpha.8",
|
|
25
|
+
"@contember/schema-utils": "^1.3.0-alpha.8",
|
|
26
26
|
"@graphql-tools/utils": "^8.6.13",
|
|
27
27
|
"graphql-tag": "^2.12.5"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@contember/engine-api-tester": "^1.3.0-alpha.
|
|
31
|
-
"@contember/schema-definition": "^1.3.0-alpha.
|
|
30
|
+
"@contember/engine-api-tester": "^1.3.0-alpha.8",
|
|
31
|
+
"@contember/schema-definition": "^1.3.0-alpha.8",
|
|
32
32
|
"@graphql-codegen/cli": "^2.6.2",
|
|
33
33
|
"@graphql-codegen/typescript": "^2.5.1",
|
|
34
34
|
"@graphql-codegen/typescript-operations": "^2.4.2",
|
package/src/SystemContainer.ts
CHANGED
|
@@ -11,9 +11,10 @@ import {
|
|
|
11
11
|
ExecutedMigrationsResolver,
|
|
12
12
|
IdentityFetcher,
|
|
13
13
|
MigrationAlterer,
|
|
14
|
+
MigrationsDatabaseMetadataResolverStoreFactory,
|
|
14
15
|
PermissionsFactory,
|
|
15
16
|
ProjectMigrator,
|
|
16
|
-
ProjectTruncateExecutor,
|
|
17
|
+
ProjectTruncateExecutor, SchemaDatabaseMetadataResolver,
|
|
17
18
|
SchemaVersionBuilder,
|
|
18
19
|
} from './model'
|
|
19
20
|
import { UuidProvider } from './utils'
|
|
@@ -79,8 +80,12 @@ export class SystemContainerFactory {
|
|
|
79
80
|
new SchemaDiffer(schemaMigrator))
|
|
80
81
|
.addService('migrationDescriber', ({ modificationHandlerFactory }) =>
|
|
81
82
|
new MigrationDescriber(modificationHandlerFactory))
|
|
82
|
-
.addService('
|
|
83
|
-
new
|
|
83
|
+
.addService('databaseMetadataResolver', () =>
|
|
84
|
+
new SchemaDatabaseMetadataResolver())
|
|
85
|
+
.addService('migrationsDatabaseMetadataResolverStoreFactory', ({ databaseMetadataResolver }) =>
|
|
86
|
+
new MigrationsDatabaseMetadataResolverStoreFactory(databaseMetadataResolver))
|
|
87
|
+
.addService('projectMigrator', ({ migrationDescriber, schemaVersionBuilder, executedMigrationsResolver, migrationsDatabaseMetadataResolverStoreFactory }) =>
|
|
88
|
+
new ProjectMigrator(migrationDescriber, schemaVersionBuilder, executedMigrationsResolver, migrationsDatabaseMetadataResolverStoreFactory))
|
|
84
89
|
.addService('projectTruncateExecutor', () =>
|
|
85
90
|
new ProjectTruncateExecutor())
|
|
86
91
|
.addService('migrationAlterer', () =>
|
package/src/bin/runMigrations.ts
CHANGED
|
@@ -3,7 +3,11 @@ import { DatabaseContextFactory, emptyVersionedSchema, SchemaVersionBuilder } fr
|
|
|
3
3
|
import { Connection } from '@contember/database'
|
|
4
4
|
import { createLogger, PrettyPrintLoggerHandler } from '@contember/logger'
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
|
|
7
|
+
import { SchemaDatabaseMetadataResolver } from '../model/metadata/SchemaDatabaseMetadataResolver'
|
|
8
|
+
import { dummySchemaDatabaseMetadata } from '@contember/schema-utils';
|
|
9
|
+
|
|
10
|
+
(async () => {
|
|
7
11
|
const dbConfig = {
|
|
8
12
|
database: process.env.PGDATABASE as string,
|
|
9
13
|
host: process.env.PGHOST as string,
|
|
@@ -25,6 +29,9 @@ import { createLogger, PrettyPrintLoggerHandler } from '@contember/logger'
|
|
|
25
29
|
buildSchema: () => Promise.resolve(emptyVersionedSchema),
|
|
26
30
|
} as unknown as SchemaVersionBuilder,
|
|
27
31
|
{},
|
|
32
|
+
{
|
|
33
|
+
resolveMetadata: () => Promise.resolve(dummySchemaDatabaseMetadata),
|
|
34
|
+
} as unknown as SchemaDatabaseMetadataResolver,
|
|
28
35
|
)
|
|
29
36
|
// eslint-disable-next-line no-console
|
|
30
37
|
await migrationsRunner.run(createLogger(new PrettyPrintLoggerHandler(process.stderr)))
|
package/src/index.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { MigrationArgs, MigrationBuilder } from '@contember/database-migrations'
|
|
2
2
|
import { wrapIdentifier } from '@contember/database'
|
|
3
3
|
import { SystemMigrationArgs } from './types'
|
|
4
|
-
import { acceptFieldVisitor,
|
|
4
|
+
import { acceptFieldVisitor, SchemaDatabaseMetadata } from '@contember/schema-utils'
|
|
5
5
|
import { Model } from '@contember/schema'
|
|
6
6
|
|
|
7
7
|
export default async function (builder: MigrationBuilder, args: MigrationArgs<SystemMigrationArgs>) {
|
|
8
8
|
const schema = await args.schemaResolver(args.connection)
|
|
9
|
-
const stages = (await args.connection.query('SELECT schema FROM stage')).rows
|
|
9
|
+
const stages = (await args.connection.query<{schema: string}>('SELECT schema FROM stage')).rows
|
|
10
|
+
const metadataByStage: Record<string, SchemaDatabaseMetadata> = {}
|
|
11
|
+
|
|
10
12
|
for (const entity of Object.values(schema.model.entities)) {
|
|
11
13
|
for (const field of Object.values(entity.fields)) {
|
|
12
14
|
const result = acceptFieldVisitor<null | {
|
|
@@ -36,30 +38,27 @@ export default async function (builder: MigrationBuilder, args: MigrationArgs<Sy
|
|
|
36
38
|
continue
|
|
37
39
|
}
|
|
38
40
|
for (const stage of stages) {
|
|
39
|
-
const
|
|
40
|
-
entity.tableName,
|
|
41
|
-
relation.joiningColumn.columnName,
|
|
42
|
-
targetEntity.tableName,
|
|
43
|
-
targetEntity.primaryColumn,
|
|
44
|
-
)
|
|
45
|
-
const tableName = { name: entity.tableName, schema: stage.schema }
|
|
41
|
+
const databaseMetadata = metadataByStage[stage.schema] ??= await args.databaseMetadataResolver(args.connection, stage.schema)
|
|
46
42
|
|
|
47
|
-
|
|
43
|
+
const fkNames = databaseMetadata.getForeignKeyConstraintNames({
|
|
44
|
+
fromTable: entity.tableName,
|
|
45
|
+
fromColumn: relation.joiningColumn.columnName,
|
|
46
|
+
toTable: targetEntity.tableName,
|
|
47
|
+
toColumn: targetEntity.primaryColumn,
|
|
48
|
+
})
|
|
49
|
+
for (const name of fkNames) {
|
|
50
|
+
builder.sql(`ALTER TABLE ${wrapIdentifier(stage.schema)}.${wrapIdentifier(entity.tableName)} DROP CONSTRAINT ${wrapIdentifier(name)}`)
|
|
51
|
+
}
|
|
48
52
|
|
|
49
53
|
const onDelete = ({
|
|
50
54
|
[Model.OnDelete.setNull]: 'SET NULL',
|
|
51
55
|
[Model.OnDelete.cascade]: 'CASCADE',
|
|
52
56
|
} as const)[relation.joiningColumn.onDelete]
|
|
53
57
|
|
|
54
|
-
builder.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
onDelete: onDelete,
|
|
59
|
-
},
|
|
60
|
-
deferrable: true,
|
|
61
|
-
deferred: false,
|
|
62
|
-
})
|
|
58
|
+
builder.sql(`ALTER TABLE ${wrapIdentifier(stage.schema)}.${wrapIdentifier(entity.tableName)}
|
|
59
|
+
ADD FOREIGN KEY (${wrapIdentifier(relation.joiningColumn.columnName)})
|
|
60
|
+
REFERENCES ${wrapIdentifier(stage.schema)}.${wrapIdentifier(targetEntity.tableName)} (${wrapIdentifier(targetEntity.primaryColumn)}) ON DELETE ${onDelete} DEFERRABLE INITIALLY IMMEDIATE
|
|
61
|
+
`)
|
|
63
62
|
}
|
|
64
63
|
}
|
|
65
64
|
}
|
package/src/migrations/runner.ts
CHANGED
|
@@ -25,6 +25,7 @@ import { ProjectConfig } from '../types'
|
|
|
25
25
|
import { Logger } from '@contember/logger'
|
|
26
26
|
import { GroupMigrationsResolver } from '@contember/database-migrations'
|
|
27
27
|
import { SnapshotMigrationResolver } from '@contember/database-migrations'
|
|
28
|
+
import { SchemaDatabaseMetadataResolver } from '../model/metadata/SchemaDatabaseMetadataResolver'
|
|
28
29
|
|
|
29
30
|
const migrations = {
|
|
30
31
|
'2018-08-04-102200-init': _20180804102200init,
|
|
@@ -55,6 +56,7 @@ export class SystemMigrationsRunner {
|
|
|
55
56
|
private readonly schema: string,
|
|
56
57
|
private readonly schemaVersionBuilder: SchemaVersionBuilder,
|
|
57
58
|
private readonly migrationGroups: Record<string, MigrationGroup<unknown>>,
|
|
59
|
+
private readonly databaseMetadataResolver: SchemaDatabaseMetadataResolver,
|
|
58
60
|
) {
|
|
59
61
|
}
|
|
60
62
|
async run(logger: Logger) {
|
|
@@ -70,10 +72,14 @@ export class SystemMigrationsRunner {
|
|
|
70
72
|
Object.fromEntries(Object.entries(this.migrationGroups).map(([group, it]) => [group, new SnapshotMigrationResolver(it.snapshot, it.migrations, group.replace(/[^-_\w]+/g, '-'))])),
|
|
71
73
|
)
|
|
72
74
|
const migrationsRunner = new DbMigrationsRunner(connection, this.schema, migrationResolver)
|
|
73
|
-
|
|
74
75
|
await migrationsRunner.migrate(message => logger.warn(message), {
|
|
75
76
|
project: this.project,
|
|
76
77
|
schemaResolver,
|
|
78
|
+
databaseMetadataResolver: (connection: Connection.ConnectionLike, schema: string) => {
|
|
79
|
+
const dbContextMigrations = this.databaseContextFactory.create(connection)
|
|
80
|
+
return this.databaseMetadataResolver.resolveMetadata(dbContextMigrations, schema)
|
|
81
|
+
},
|
|
82
|
+
|
|
77
83
|
})
|
|
78
84
|
})
|
|
79
85
|
await singleConnection.end()
|
package/src/migrations/types.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { Schema } from '@contember/schema'
|
|
2
2
|
import { ProjectConfig } from '../types'
|
|
3
3
|
import { Connection } from '@contember/database'
|
|
4
|
+
import { SchemaDatabaseMetadata } from '@contember/schema-utils'
|
|
4
5
|
|
|
5
6
|
export interface SystemMigrationArgs {
|
|
6
7
|
schemaResolver: (connection: Connection.ConnectionLike) => Promise<Schema>
|
|
8
|
+
databaseMetadataResolver: (connection: Connection.ConnectionLike, schema: string) => Promise<SchemaDatabaseMetadata>
|
|
7
9
|
project: ProjectConfig
|
|
8
10
|
}
|
package/src/model/index.ts
CHANGED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ForeignKeyConstraintMetadata,
|
|
3
|
+
GetForeignKeyConstraintNameArgs,
|
|
4
|
+
GetIndexNameArgs,
|
|
5
|
+
GetUniqueConstraintNameArgs,
|
|
6
|
+
IndexMetadata,
|
|
7
|
+
SchemaDatabaseMetadata,
|
|
8
|
+
UniqueConstraintMetadata,
|
|
9
|
+
} from '@contember/schema-utils'
|
|
10
|
+
|
|
11
|
+
const stringArrayEquals = (colA: string[], colB: string[]) => {
|
|
12
|
+
return colA.length === colB.length
|
|
13
|
+
&& colA.every((it, index) => it === colB[index])
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class ResolvedDatabaseMetadata implements SchemaDatabaseMetadata {
|
|
17
|
+
constructor(
|
|
18
|
+
private readonly foreignKeys: ForeignKeyConstraintMetadata[],
|
|
19
|
+
private readonly uniqueConstraints: UniqueConstraintMetadata[],
|
|
20
|
+
private readonly indexes: IndexMetadata[],
|
|
21
|
+
) {
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
getUniqueConstraint(tableName: string, constraintName: string): UniqueConstraintMetadata | null {
|
|
25
|
+
return this.uniqueConstraints.find(it => it.tableName === tableName && it.constraintName === constraintName) ?? null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
getForeignKeyConstraint(tableName: string, constraintName: string): ForeignKeyConstraintMetadata | null {
|
|
29
|
+
return this.foreignKeys.find(it => it.fromTable === tableName && it.constraintName === constraintName) ?? null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
getForeignKeyConstraintNames({ fromTable, fromColumn, toTable, toColumn }: GetForeignKeyConstraintNameArgs): string[] {
|
|
33
|
+
return this.foreignKeys
|
|
34
|
+
.filter(it =>
|
|
35
|
+
it.fromTable === fromTable
|
|
36
|
+
&& it.toTable === toTable
|
|
37
|
+
&& it.fromColumn === fromColumn
|
|
38
|
+
&& it.toColumn === toColumn,
|
|
39
|
+
)
|
|
40
|
+
.map(it => it.constraintName)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
getUniqueConstraintNames({ tableName, columnNames }: GetUniqueConstraintNameArgs): string[] {
|
|
44
|
+
return this.uniqueConstraints
|
|
45
|
+
.filter(it => it.tableName === tableName && stringArrayEquals(columnNames, it.columnNames))
|
|
46
|
+
.map(it => it.constraintName)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
getIndexNames({ tableName, columnNames }: GetIndexNameArgs): string[] {
|
|
50
|
+
return this.indexes
|
|
51
|
+
.filter(it => it.tableName === tableName && stringArrayEquals(columnNames, it.columnNames))
|
|
52
|
+
.map(it => it.indexName)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ForeignKeyConstraintMetadata,
|
|
3
|
+
IndexMetadata,
|
|
4
|
+
SchemaDatabaseMetadata,
|
|
5
|
+
UniqueConstraintMetadata,
|
|
6
|
+
} from '@contember/schema-utils'
|
|
7
|
+
import { DatabaseContext } from '../database'
|
|
8
|
+
import { Connection } from '@contember/database'
|
|
9
|
+
import { ResolvedDatabaseMetadata } from './ResolvedDatabaseMetadata'
|
|
10
|
+
|
|
11
|
+
export class SchemaDatabaseMetadataResolver {
|
|
12
|
+
async resolveMetadata(db: DatabaseContext, contentSchema: string): Promise<SchemaDatabaseMetadata> {
|
|
13
|
+
const constraintRows = await this.fetchConstraints(db.client, contentSchema)
|
|
14
|
+
const indexRows = await this.fetchIndexes(db.client, contentSchema)
|
|
15
|
+
|
|
16
|
+
const fkConstraints = constraintRows
|
|
17
|
+
.filter((it): it is ForeignKeyConstraintsRow => it.type === ConstraintTypes.foreignKey)
|
|
18
|
+
.map((it): ForeignKeyConstraintMetadata => ({
|
|
19
|
+
constraintName: it.constraint_name,
|
|
20
|
+
fromColumn: it.columns[0],
|
|
21
|
+
toColumn: it.target_columns[0],
|
|
22
|
+
fromTable: it.table_name,
|
|
23
|
+
toTable: it.target_table,
|
|
24
|
+
}))
|
|
25
|
+
const uniqueConstraints = constraintRows
|
|
26
|
+
.filter((it): it is UniqueConstraintsRow => it.type === ConstraintTypes.unique)
|
|
27
|
+
.map((it): UniqueConstraintMetadata => ({
|
|
28
|
+
constraintName: it.constraint_name,
|
|
29
|
+
tableName: it.table_name,
|
|
30
|
+
columnNames: it.columns,
|
|
31
|
+
}))
|
|
32
|
+
const indexes = indexRows
|
|
33
|
+
.map((it): IndexMetadata => ({
|
|
34
|
+
indexName: it.index_name,
|
|
35
|
+
tableName: it.table_name,
|
|
36
|
+
columnNames: it.columns,
|
|
37
|
+
}))
|
|
38
|
+
|
|
39
|
+
return new ResolvedDatabaseMetadata(
|
|
40
|
+
fkConstraints,
|
|
41
|
+
uniqueConstraints,
|
|
42
|
+
indexes,
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private async fetchConstraints(connection: Connection.Queryable, schema: string): Promise<ConstraintsRow[]> {
|
|
47
|
+
return (await connection.query<ConstraintsRow>(`
|
|
48
|
+
SELECT
|
|
49
|
+
MAX(pg_constraint.conname) AS constraint_name,
|
|
50
|
+
MAX(pg_class.relname) AS table_name,
|
|
51
|
+
JSONB_AGG(DISTINCT pg_attribute.attname) AS columns,
|
|
52
|
+
MAX(pg_constraint.contype) AS type,
|
|
53
|
+
MAX(pg_constraint_table.relname) AS target_table,
|
|
54
|
+
JSONB_AGG(DISTINCT pg_attribute_target.attname)
|
|
55
|
+
FILTER ( WHERE pg_attribute_target.attname IS NOT NULL) AS target_columns,
|
|
56
|
+
BOOL_OR(condeferrable),
|
|
57
|
+
BOOL_OR(condeferred)
|
|
58
|
+
FROM pg_constraint
|
|
59
|
+
JOIN pg_class
|
|
60
|
+
ON pg_constraint.conrelid = pg_class.oid
|
|
61
|
+
JOIN pg_namespace
|
|
62
|
+
ON pg_class.relnamespace = pg_namespace.oid
|
|
63
|
+
JOIN pg_attribute
|
|
64
|
+
ON pg_attribute.attrelid = pg_class.oid AND pg_attribute.attnum = ANY (pg_constraint.conkey)
|
|
65
|
+
LEFT JOIN pg_attribute pg_attribute_target
|
|
66
|
+
ON pg_constraint.confrelid = pg_attribute_target.attrelid AND
|
|
67
|
+
pg_attribute_target.attnum = ANY (pg_constraint.confkey)
|
|
68
|
+
LEFT JOIN pg_class pg_constraint_table
|
|
69
|
+
ON pg_constraint_table.oid = pg_constraint.confrelid
|
|
70
|
+
WHERE pg_namespace.nspname = ?
|
|
71
|
+
AND pg_constraint.contype = ANY (ARRAY ['f', 'u'])
|
|
72
|
+
GROUP BY pg_constraint.oid
|
|
73
|
+
`, [schema]))
|
|
74
|
+
.rows
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private async fetchIndexes(connection: Connection.Queryable, schema: string): Promise<IndexRow[]> {
|
|
78
|
+
return (await connection.query<IndexRow>(`
|
|
79
|
+
SELECT
|
|
80
|
+
MAX(idx_class.relname) AS index_name,
|
|
81
|
+
MAX(table_class.relname) AS table_name,
|
|
82
|
+
JSONB_AGG(DISTINCT pg_attribute.attname) AS columns
|
|
83
|
+
FROM pg_index
|
|
84
|
+
JOIN pg_class AS table_class
|
|
85
|
+
ON pg_index.indrelid = table_class.oid
|
|
86
|
+
JOIN pg_class AS idx_class
|
|
87
|
+
ON pg_index.indexrelid = idx_class.oid
|
|
88
|
+
JOIN pg_namespace
|
|
89
|
+
ON table_class.relnamespace = pg_namespace.oid
|
|
90
|
+
JOIN pg_attribute
|
|
91
|
+
ON pg_attribute.attrelid = table_class.oid AND pg_attribute.attnum = ANY (pg_index.indkey)
|
|
92
|
+
WHERE pg_namespace.nspname = ?
|
|
93
|
+
AND indisprimary = FALSE
|
|
94
|
+
AND indisunique = FALSE
|
|
95
|
+
GROUP BY pg_index.indexrelid
|
|
96
|
+
`, [schema]))
|
|
97
|
+
.rows
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
enum ConstraintTypes {
|
|
102
|
+
foreignKey = 'f',
|
|
103
|
+
unique = 'u',
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
type ConstraintsRow =
|
|
107
|
+
| ForeignKeyConstraintsRow
|
|
108
|
+
| UniqueConstraintsRow
|
|
109
|
+
|
|
110
|
+
type ForeignKeyConstraintsRow = {
|
|
111
|
+
constraint_name: string
|
|
112
|
+
table_name: string
|
|
113
|
+
columns: string[]
|
|
114
|
+
target_table: string
|
|
115
|
+
target_columns: string[]
|
|
116
|
+
type: ConstraintTypes.foreignKey
|
|
117
|
+
deferrable: boolean
|
|
118
|
+
deferred: boolean
|
|
119
|
+
}
|
|
120
|
+
type UniqueConstraintsRow = {
|
|
121
|
+
constraint_name: string
|
|
122
|
+
table_name: string
|
|
123
|
+
columns: string[]
|
|
124
|
+
target_table: null
|
|
125
|
+
target_columns: null
|
|
126
|
+
type: ConstraintTypes.unique
|
|
127
|
+
deferrable: boolean
|
|
128
|
+
deferred: boolean
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
type IndexRow = {
|
|
132
|
+
index_name: string
|
|
133
|
+
table_name: string
|
|
134
|
+
columns: string[]
|
|
135
|
+
}
|
|
136
|
+
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { SchemaDatabaseMetadata } from '@contember/schema-utils'
|
|
2
|
+
import { SchemaDatabaseMetadataResolver } from './SchemaDatabaseMetadataResolver'
|
|
3
|
+
import { DatabaseContext } from '../database'
|
|
4
|
+
|
|
5
|
+
export class SchemaDatabaseMetadataResolverStore {
|
|
6
|
+
|
|
7
|
+
private cache: Map<string, SchemaDatabaseMetadata> = new Map()
|
|
8
|
+
|
|
9
|
+
constructor(
|
|
10
|
+
private resolver: SchemaDatabaseMetadataResolver,
|
|
11
|
+
private db: DatabaseContext,
|
|
12
|
+
) {
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
public async getMetadata(schema: string): Promise<SchemaDatabaseMetadata> {
|
|
16
|
+
const cached = this.cache.get(schema)
|
|
17
|
+
if (cached) {
|
|
18
|
+
return cached
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const newValue = await this.resolver.resolveMetadata(this.db, schema)
|
|
22
|
+
this.cache.set(schema, newValue)
|
|
23
|
+
return newValue
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public invalidate(schema: string): void {
|
|
27
|
+
this.cache.delete(schema)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
export class MigrationsDatabaseMetadataResolverStoreFactory {
|
|
33
|
+
constructor(
|
|
34
|
+
private resolver: SchemaDatabaseMetadataResolver,
|
|
35
|
+
) {
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public create(db: DatabaseContext) {
|
|
39
|
+
return new SchemaDatabaseMetadataResolverStore(this.resolver, db)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -9,12 +9,17 @@ import { MigrateErrorCode } from '../../schema'
|
|
|
9
9
|
import { SchemaVersionBuilder } from './SchemaVersionBuilder'
|
|
10
10
|
import { SchemaValidator } from '@contember/schema-utils'
|
|
11
11
|
import { logger } from '@contember/logger'
|
|
12
|
+
import {
|
|
13
|
+
SchemaDatabaseMetadataResolverStore,
|
|
14
|
+
MigrationsDatabaseMetadataResolverStoreFactory,
|
|
15
|
+
} from '../metadata/SchemaDatabaseMetadataResolverStore'
|
|
12
16
|
|
|
13
17
|
export class ProjectMigrator {
|
|
14
18
|
constructor(
|
|
15
19
|
private readonly migrationDescriber: MigrationDescriber,
|
|
16
20
|
private readonly schemaVersionBuilder: SchemaVersionBuilder,
|
|
17
21
|
private readonly executedMigrationsResolver: ExecutedMigrationsResolver,
|
|
22
|
+
private readonly migrationsDatabaseMetadataResolverStoreFactory: MigrationsDatabaseMetadataResolverStoreFactory,
|
|
18
23
|
) {}
|
|
19
24
|
|
|
20
25
|
public async migrate(
|
|
@@ -34,6 +39,8 @@ export class ProjectMigrator {
|
|
|
34
39
|
|
|
35
40
|
const sorted = [...validated].sort((a, b) => a.version.localeCompare(b.version))
|
|
36
41
|
|
|
42
|
+
const metadataStore = this.migrationsDatabaseMetadataResolverStoreFactory.create(db)
|
|
43
|
+
|
|
37
44
|
for (const migration of sorted) {
|
|
38
45
|
const formatVersion = migration.formatVersion
|
|
39
46
|
|
|
@@ -46,6 +53,7 @@ export class ProjectMigrator {
|
|
|
46
53
|
formatVersion,
|
|
47
54
|
migration.version,
|
|
48
55
|
db.client.schema,
|
|
56
|
+
metadataStore,
|
|
49
57
|
)
|
|
50
58
|
}
|
|
51
59
|
await db.commandBus.execute(new SaveMigrationCommand(migration))
|
|
@@ -76,7 +84,7 @@ export class ProjectMigrator {
|
|
|
76
84
|
if (migration.version < version && !ignoreOrder) {
|
|
77
85
|
throw new MustFollowLatestMigrationError(migration.version, `Must follow latest executed migration ${version}`)
|
|
78
86
|
}
|
|
79
|
-
const described =
|
|
87
|
+
const described = this.migrationDescriber.describeModifications(schema, migration)
|
|
80
88
|
if (described.length === 0) {
|
|
81
89
|
continue
|
|
82
90
|
}
|
|
@@ -112,13 +120,19 @@ export class ProjectMigrator {
|
|
|
112
120
|
formatVersion: number,
|
|
113
121
|
migrationVersion: string,
|
|
114
122
|
systemSchema: string,
|
|
123
|
+
metadataStore: SchemaDatabaseMetadataResolverStore,
|
|
115
124
|
): Promise<[Schema]> {
|
|
116
125
|
const {
|
|
117
|
-
|
|
126
|
+
getSql,
|
|
118
127
|
schema: newSchema,
|
|
119
|
-
|
|
120
|
-
} = await this.migrationDescriber.describeModification(schema, modification, { systemSchema, formatVersion })
|
|
128
|
+
} = this.migrationDescriber.describeModification(schema, modification, { formatVersion })
|
|
121
129
|
for (const stage of stages) {
|
|
130
|
+
const databaseMetadata = await metadataStore.getMetadata(stage.schema)
|
|
131
|
+
const sql = getSql({
|
|
132
|
+
systemSchema,
|
|
133
|
+
databaseMetadata,
|
|
134
|
+
invalidateDatabaseMetadata: () => metadataStore.invalidate(stage.schema),
|
|
135
|
+
})
|
|
122
136
|
await this.executeOnStage(db, stage, sql, migrationVersion)
|
|
123
137
|
}
|
|
124
138
|
return [newSchema]
|
|
@@ -2,12 +2,14 @@ import migration from '../../../src/migrations/2020-05-06-150000-composed-primar
|
|
|
2
2
|
import { createMigrationBuilder } from '@contember/database-migrations'
|
|
3
3
|
import { sampleProject } from '@contember/engine-api-tester'
|
|
4
4
|
import { test, assert } from 'vitest'
|
|
5
|
+
import { dummySchemaDatabaseMetadata } from '@contember/schema-utils'
|
|
5
6
|
|
|
6
7
|
test('many-has-many-primary migration sql', async () => {
|
|
7
8
|
const builder = createMigrationBuilder()
|
|
8
9
|
await migration(builder, {
|
|
9
10
|
connection: undefined as any,
|
|
10
11
|
schemaResolver: () => Promise.resolve(sampleProject),
|
|
12
|
+
databaseMetadataResolver: () => Promise.resolve(dummySchemaDatabaseMetadata),
|
|
11
13
|
project: {
|
|
12
14
|
slug: 'test',
|
|
13
15
|
stages: [
|
|
@@ -2,6 +2,7 @@ import migration from '../../../src/migrations/2020-06-01-103000-event-trigger-p
|
|
|
2
2
|
import { createMigrationBuilder } from '@contember/database-migrations'
|
|
3
3
|
import { sampleProject } from '@contember/engine-api-tester'
|
|
4
4
|
import { test, assert } from 'vitest'
|
|
5
|
+
import { dummySchemaDatabaseMetadata } from '@contember/schema-utils'
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
test('event-trigger-performance sql', async () => {
|
|
@@ -9,6 +10,7 @@ test('event-trigger-performance sql', async () => {
|
|
|
9
10
|
await migration(builder, {
|
|
10
11
|
connection: undefined as any,
|
|
11
12
|
schemaResolver: () => Promise.resolve(sampleProject),
|
|
13
|
+
databaseMetadataResolver: () => Promise.resolve(dummySchemaDatabaseMetadata),
|
|
12
14
|
project: {
|
|
13
15
|
slug: 'test',
|
|
14
16
|
stages: [
|
|
@@ -2,7 +2,7 @@ import migration from '../../../src/migrations/2022-10-03-110000-table-on-delete
|
|
|
2
2
|
import { createMigrationBuilder } from '@contember/database-migrations'
|
|
3
3
|
import { assert, test } from 'vitest'
|
|
4
4
|
import { SchemaBuilder } from '@contember/schema-definition'
|
|
5
|
-
import { emptySchema } from '@contember/schema-utils'
|
|
5
|
+
import { dummySchemaDatabaseMetadata, emptySchema } from '@contember/schema-utils'
|
|
6
6
|
import { Model } from '@contember/schema'
|
|
7
7
|
import { createConnectionMock } from '@contember/database-tester'
|
|
8
8
|
|
|
@@ -15,6 +15,7 @@ test('table-on-delete test', async () => {
|
|
|
15
15
|
}])
|
|
16
16
|
await migration(builder, {
|
|
17
17
|
connection: connection,
|
|
18
|
+
databaseMetadataResolver: () => Promise.resolve(dummySchemaDatabaseMetadata),
|
|
18
19
|
schemaResolver: () => Promise.resolve(({ ...emptySchema, model: new SchemaBuilder()
|
|
19
20
|
.entity('Post', entity =>
|
|
20
21
|
entity.manyHasOne('author', relation => relation.target('Author').onDelete(Model.OnDelete.setNull)),
|
|
@@ -33,9 +34,11 @@ test('table-on-delete test', async () => {
|
|
|
33
34
|
})
|
|
34
35
|
assert.equal(
|
|
35
36
|
builder.getSql(),
|
|
36
|
-
`ALTER TABLE "stage_live"."post" DROP CONSTRAINT "
|
|
37
|
+
`ALTER TABLE "stage_live"."post" DROP CONSTRAINT "fk_post_author_id_author_id";
|
|
37
38
|
ALTER TABLE "stage_live"."post"
|
|
38
|
-
|
|
39
|
+
ADD FOREIGN KEY ("author_id")
|
|
40
|
+
REFERENCES "stage_live"."author" ("id") ON DELETE SET NULL DEFERRABLE INITIALLY IMMEDIATE
|
|
41
|
+
;
|
|
39
42
|
`,
|
|
40
43
|
)
|
|
41
44
|
})
|