@contember/schema-migrations 1.4.0-rc.2 → 1.4.0-rc.4
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/modifications/columns/CreateColumnModification.d.ts +1 -0
- package/dist/src/modifications/columns/CreateColumnModification.d.ts.map +1 -1
- package/dist/src/modifications/columns/CreateColumnModification.js +27 -15
- package/dist/src/modifications/columns/CreateColumnModification.js.map +1 -1
- package/dist/src/modifications/columns/UpdateColumnDefinitionModification.d.ts +1 -0
- package/dist/src/modifications/columns/UpdateColumnDefinitionModification.d.ts.map +1 -1
- package/dist/src/modifications/columns/UpdateColumnDefinitionModification.js +27 -13
- package/dist/src/modifications/columns/UpdateColumnDefinitionModification.js.map +1 -1
- package/dist/src/modifications/columns/columnUtils.d.ts +9 -5
- package/dist/src/modifications/columns/columnUtils.d.ts.map +1 -1
- package/dist/src/modifications/columns/columnUtils.js +15 -15
- package/dist/src/modifications/columns/columnUtils.js.map +1 -1
- package/dist/src/tsconfig.tsbuildinfo +1 -1
- package/dist/tests/cases/integration/createColumn.test.js +37 -2
- package/dist/tests/cases/integration/createColumn.test.js.map +1 -1
- package/dist/tests/cases/integration/updateColumnDefinition.test.js +19 -0
- package/dist/tests/cases/integration/updateColumnDefinition.test.js.map +1 -1
- package/dist/tests/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/src/modifications/columns/CreateColumnModification.ts +32 -16
- package/src/modifications/columns/UpdateColumnDefinitionModification.ts +30 -14
- package/src/modifications/columns/columnUtils.ts +22 -16
- package/tests/cases/integration/createColumn.test.ts +35 -2
- package/tests/cases/integration/updateColumnDefinition.test.ts +20 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contember/schema-migrations",
|
|
3
|
-
"version": "1.4.0-rc.
|
|
3
|
+
"version": "1.4.0-rc.4",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"main": "dist/src/index.js",
|
|
6
6
|
"typings": "dist/src/index.d.ts",
|
|
@@ -12,16 +12,16 @@
|
|
|
12
12
|
"test": "vitest --dir ./tests/cases"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@contember/database": "1.4.0-rc.
|
|
16
|
-
"@contember/database-migrations": "1.4.0-rc.
|
|
17
|
-
"@contember/engine-common": "1.4.0-rc.
|
|
18
|
-
"@contember/schema": "1.4.0-rc.
|
|
19
|
-
"@contember/schema-utils": "1.4.0-rc.
|
|
15
|
+
"@contember/database": "1.4.0-rc.4",
|
|
16
|
+
"@contember/database-migrations": "1.4.0-rc.4",
|
|
17
|
+
"@contember/engine-common": "1.4.0-rc.4",
|
|
18
|
+
"@contember/schema": "1.4.0-rc.4",
|
|
19
|
+
"@contember/schema-utils": "1.4.0-rc.4",
|
|
20
20
|
"fast-deep-equal": "^3.1.3",
|
|
21
21
|
"rfc6902": "^5.0.1"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@contember/schema-definition": "1.4.0-rc.
|
|
24
|
+
"@contember/schema-definition": "1.4.0-rc.4"
|
|
25
25
|
},
|
|
26
26
|
"peerDependencies": {
|
|
27
27
|
"pg": "^8.9.0"
|
|
@@ -5,7 +5,7 @@ import { createModificationType, Differ, ModificationHandler } from '../Modifica
|
|
|
5
5
|
import { isColumn } from '@contember/schema-utils'
|
|
6
6
|
import { createFields } from '../utils/diffUtils'
|
|
7
7
|
import { getColumnSqlType } from '../utils/columnUtils'
|
|
8
|
-
import { fillSeed } from './columnUtils'
|
|
8
|
+
import { fillSeed, formatSeedExpression } from './columnUtils'
|
|
9
9
|
|
|
10
10
|
export class CreateColumnModificationHandler implements ModificationHandler<CreateColumnModificationData> {
|
|
11
11
|
constructor(private readonly data: CreateColumnModificationData, private readonly schema: Schema) {}
|
|
@@ -16,28 +16,43 @@ export class CreateColumnModificationHandler implements ModificationHandler<Crea
|
|
|
16
16
|
return
|
|
17
17
|
}
|
|
18
18
|
const column = this.data.field
|
|
19
|
-
|
|
19
|
+
|
|
20
20
|
const columnType = getColumnSqlType(column)
|
|
21
|
+
|
|
22
|
+
const seedExpression = formatSeedExpression({
|
|
23
|
+
model: this.schema.model,
|
|
24
|
+
entity,
|
|
25
|
+
columnType,
|
|
26
|
+
fillValue: this.data.fillValue,
|
|
27
|
+
copyValue: this.data.copyValue,
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
|
|
21
31
|
builder.addColumn(entity.tableName, {
|
|
22
32
|
[column.columnName]: {
|
|
23
33
|
type: columnType,
|
|
24
|
-
notNull: !column.nullable &&
|
|
34
|
+
notNull: !column.nullable && seedExpression === null,
|
|
25
35
|
sequenceGenerated: column.sequence,
|
|
26
36
|
},
|
|
27
37
|
})
|
|
28
38
|
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
-
builder,
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
if (seedExpression !== null) {
|
|
40
|
+
if (this.data.valueMigrationStrategy === 'using') {
|
|
41
|
+
builder.alterColumn(entity.tableName, column.columnName, {
|
|
42
|
+
notNull: !column.nullable ? true : undefined,
|
|
43
|
+
type: columnType,
|
|
44
|
+
using: seedExpression,
|
|
45
|
+
})
|
|
46
|
+
} else {
|
|
47
|
+
fillSeed({
|
|
48
|
+
builder,
|
|
49
|
+
type: 'creating',
|
|
50
|
+
entity,
|
|
51
|
+
columnName: column.columnName,
|
|
52
|
+
nullable: column.nullable,
|
|
53
|
+
seedExpression,
|
|
54
|
+
})
|
|
55
|
+
}
|
|
41
56
|
}
|
|
42
57
|
}
|
|
43
58
|
|
|
@@ -61,6 +76,7 @@ export interface CreateColumnModificationData {
|
|
|
61
76
|
field: Model.AnyColumn
|
|
62
77
|
fillValue?: JSONValue
|
|
63
78
|
copyValue?: string
|
|
79
|
+
valueMigrationStrategy?: 'using' | 'update'
|
|
64
80
|
}
|
|
65
81
|
|
|
66
82
|
|
|
@@ -78,7 +94,7 @@ export class CreateColumnDiffer implements Differ {
|
|
|
78
94
|
return createColumnModification.createModification({
|
|
79
95
|
entityName: updatedEntity.name,
|
|
80
96
|
field: newField,
|
|
81
|
-
...(newField.default !== undefined ? { fillValue: newField.default } : {}),
|
|
97
|
+
...(newField.default !== undefined ? { fillValue: newField.default, valueMigrationStrategy: 'using' } : {}),
|
|
82
98
|
})
|
|
83
99
|
})
|
|
84
100
|
}
|
|
@@ -6,7 +6,7 @@ import deepEqual from 'fast-deep-equal'
|
|
|
6
6
|
import { updateColumns } from '../utils/diffUtils'
|
|
7
7
|
import { wrapIdentifier } from '../../utils/dbHelpers'
|
|
8
8
|
import { getColumnSqlType } from '../utils/columnUtils'
|
|
9
|
-
import { fillSeed } from './columnUtils'
|
|
9
|
+
import { fillSeed, formatSeedExpression } from './columnUtils'
|
|
10
10
|
|
|
11
11
|
export class UpdateColumnDefinitionModificationHandler implements ModificationHandler<UpdateColumnDefinitionModificationData> {
|
|
12
12
|
constructor(private readonly data: UpdateColumnDefinitionModificationData, private readonly schema: Schema) {}
|
|
@@ -22,33 +22,48 @@ export class UpdateColumnDefinitionModificationHandler implements ModificationHa
|
|
|
22
22
|
const hasNewSequence = !oldColumn.sequence && newColumn.sequence
|
|
23
23
|
const hasNewType = newColumn.columnType !== oldColumn.columnType
|
|
24
24
|
const columnType = getColumnSqlType(newColumn)
|
|
25
|
+
const hasNullableChanged = oldColumn.nullable !== newColumn.nullable
|
|
25
26
|
|
|
26
27
|
const usingCast = `${wrapIdentifier(oldColumn.columnName)}::${columnType}`
|
|
27
28
|
|
|
28
|
-
const
|
|
29
|
+
const seedExpression = formatSeedExpression({
|
|
30
|
+
model: this.schema.model,
|
|
31
|
+
entity,
|
|
32
|
+
columnType,
|
|
33
|
+
fillValue: this.data.fillValue,
|
|
34
|
+
copyValue: this.data.copyValue,
|
|
35
|
+
})
|
|
36
|
+
const hasSeed = seedExpression !== null
|
|
37
|
+
const migrateWithUsing = hasSeed && this.data.valueMigrationStrategy === 'using'
|
|
38
|
+
const migrateWithUpdate = hasSeed && this.data.valueMigrationStrategy !== 'using'
|
|
29
39
|
|
|
30
40
|
builder.alterColumn(entity.tableName, oldColumn.columnName, {
|
|
31
|
-
type: hasNewSequence || hasNewType ? columnType : undefined,
|
|
32
|
-
notNull:
|
|
33
|
-
using:
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
41
|
+
type: hasNewSequence || hasNewType || migrateWithUsing ? columnType : undefined,
|
|
42
|
+
notNull: hasNullableChanged && (!migrateWithUpdate || newColumn.nullable) ? !newColumn.nullable : undefined,
|
|
43
|
+
using: (() => {
|
|
44
|
+
if (hasNewSequence) {
|
|
45
|
+
return `COALESCE(${usingCast}, nextval(PG_GET_SERIAL_SEQUENCE(${escapeValue(entity.tableName)}, ${escapeValue(oldColumn.columnName)})))`
|
|
46
|
+
}
|
|
47
|
+
if (migrateWithUsing) {
|
|
48
|
+
return `COALESCE(${usingCast}, ${seedExpression})`
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (hasNewType) {
|
|
52
|
+
return usingCast
|
|
53
|
+
}
|
|
54
|
+
return undefined
|
|
55
|
+
})(),
|
|
38
56
|
sequenceGenerated: oldColumn.sequence && !newColumn.sequence ? false : (!oldColumn.sequence ? newColumn.sequence : undefined),
|
|
39
57
|
})
|
|
40
58
|
|
|
41
|
-
if (
|
|
59
|
+
if (migrateWithUpdate) {
|
|
42
60
|
fillSeed({
|
|
43
61
|
builder,
|
|
44
62
|
type: 'updating',
|
|
45
|
-
model: this.schema.model,
|
|
46
63
|
entity,
|
|
47
64
|
columnName: oldColumn.columnName,
|
|
48
65
|
nullable: newColumn.nullable,
|
|
49
|
-
|
|
50
|
-
copyValue: this.data.copyValue,
|
|
51
|
-
fillValue: this.data.fillValue,
|
|
66
|
+
seedExpression,
|
|
52
67
|
})
|
|
53
68
|
}
|
|
54
69
|
|
|
@@ -114,6 +129,7 @@ export interface UpdateColumnDefinitionModificationData {
|
|
|
114
129
|
definition: ColumnDefinitionAlter
|
|
115
130
|
fillValue?: JSONValue
|
|
116
131
|
copyValue?: string
|
|
132
|
+
valueMigrationStrategy?: 'using' | 'update'
|
|
117
133
|
}
|
|
118
134
|
|
|
119
135
|
export const updateColumnDefinitionModification = createModificationType({
|
|
@@ -1,31 +1,20 @@
|
|
|
1
1
|
import { wrapIdentifier } from '../../utils/dbHelpers'
|
|
2
|
-
import { ImplementationException } from '../../exceptions'
|
|
3
2
|
import { JSONValue, Model } from '@contember/schema'
|
|
4
3
|
import { escapeValue, MigrationBuilder } from '@contember/database-migrations'
|
|
5
4
|
import { getColumnName } from '@contember/schema-utils'
|
|
6
5
|
|
|
7
|
-
export const fillSeed = ({ builder,
|
|
8
|
-
model: Model.Schema
|
|
6
|
+
export const fillSeed = ({ builder, entity, columnName, nullable, type, seedExpression }: {
|
|
9
7
|
entity: Model.Entity
|
|
10
8
|
columnName: string
|
|
11
|
-
columnType: string
|
|
12
9
|
nullable: boolean
|
|
13
10
|
builder: MigrationBuilder
|
|
14
|
-
fillValue?: JSONValue
|
|
15
|
-
copyValue?: string
|
|
16
11
|
type: 'creating' | 'updating'
|
|
12
|
+
seedExpression: string
|
|
17
13
|
}) => {
|
|
14
|
+
|
|
18
15
|
const where = type === 'updating' ? ` WHERE ${wrapIdentifier(columnName)} IS NULL` : ''
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
SET ${wrapIdentifier(columnName)} = ${escapeValue(fillValue)}${where}`)
|
|
22
|
-
} else if (copyValue !== undefined) {
|
|
23
|
-
const copyFrom = getColumnName(model, entity, copyValue)
|
|
24
|
-
builder.sql(`UPDATE ${wrapIdentifier(entity.tableName)}
|
|
25
|
-
SET ${wrapIdentifier(columnName)} = ${wrapIdentifier(copyFrom)}::${columnType}${where}`)
|
|
26
|
-
} else {
|
|
27
|
-
throw new ImplementationException()
|
|
28
|
-
}
|
|
16
|
+
builder.sql(`UPDATE ${wrapIdentifier(entity.tableName)}
|
|
17
|
+
SET ${wrapIdentifier(columnName)} = ${seedExpression}${where}`)
|
|
29
18
|
|
|
30
19
|
// event log uses deferred constraint triggers, we need to fire them before ALTER
|
|
31
20
|
builder.sql(`SET CONSTRAINTS ALL IMMEDIATE`)
|
|
@@ -37,3 +26,20 @@ export const fillSeed = ({ builder, fillValue, copyValue, entity, columnName, nu
|
|
|
37
26
|
})
|
|
38
27
|
}
|
|
39
28
|
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
export const formatSeedExpression = ({ model, entity, columnType, fillValue, copyValue }: {
|
|
32
|
+
model: Model.Schema
|
|
33
|
+
entity: Model.Entity
|
|
34
|
+
columnType: string
|
|
35
|
+
fillValue?: JSONValue
|
|
36
|
+
copyValue?: string
|
|
37
|
+
}): string | null => {
|
|
38
|
+
if (fillValue !== undefined) {
|
|
39
|
+
return escapeValue(fillValue)
|
|
40
|
+
} else if (copyValue !== undefined) {
|
|
41
|
+
const copyFrom = getColumnName(model, entity, copyValue)
|
|
42
|
+
return `${wrapIdentifier(copyFrom)}::${columnType}`
|
|
43
|
+
}
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
@@ -11,10 +11,11 @@ testMigrations('create a column with default value', {
|
|
|
11
11
|
}),
|
|
12
12
|
updated: createSchema({
|
|
13
13
|
Author: class Author {
|
|
14
|
-
name = def.stringColumn().
|
|
14
|
+
name = def.stringColumn().notNull()
|
|
15
15
|
email = def.stringColumn().unique()
|
|
16
16
|
},
|
|
17
17
|
}),
|
|
18
|
+
noDiff: true,
|
|
18
19
|
diff: [
|
|
19
20
|
{
|
|
20
21
|
modification: 'createColumn',
|
|
@@ -23,7 +24,6 @@ testMigrations('create a column with default value', {
|
|
|
23
24
|
columnName: 'name',
|
|
24
25
|
name: 'name',
|
|
25
26
|
nullable: false,
|
|
26
|
-
default: 'unnamed author',
|
|
27
27
|
type: Model.ColumnType.String,
|
|
28
28
|
columnType: 'text',
|
|
29
29
|
},
|
|
@@ -68,3 +68,36 @@ UPDATE "author" SET "name" = "email"::text;
|
|
|
68
68
|
SET CONSTRAINTS ALL IMMEDIATE; SET CONSTRAINTS ALL DEFERRED;
|
|
69
69
|
ALTER TABLE "author" ALTER "name" SET NOT NULL;`,
|
|
70
70
|
})
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
testMigrations('create a column with default value with "using"', {
|
|
74
|
+
original: createSchema({
|
|
75
|
+
Author: class Author {
|
|
76
|
+
email = def.stringColumn().unique()
|
|
77
|
+
},
|
|
78
|
+
}),
|
|
79
|
+
updated: createSchema({
|
|
80
|
+
Author: class Author {
|
|
81
|
+
name = def.stringColumn().default('unnamed author').notNull()
|
|
82
|
+
email = def.stringColumn().unique()
|
|
83
|
+
},
|
|
84
|
+
}),
|
|
85
|
+
diff: [
|
|
86
|
+
{
|
|
87
|
+
modification: 'createColumn',
|
|
88
|
+
entityName: 'Author',
|
|
89
|
+
field: {
|
|
90
|
+
columnName: 'name',
|
|
91
|
+
name: 'name',
|
|
92
|
+
nullable: false,
|
|
93
|
+
default: 'unnamed author',
|
|
94
|
+
type: Model.ColumnType.String,
|
|
95
|
+
columnType: 'text',
|
|
96
|
+
},
|
|
97
|
+
fillValue: 'unnamed author',
|
|
98
|
+
valueMigrationStrategy: 'using',
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
sql: SQL`ALTER TABLE "author" ADD "name" text;
|
|
102
|
+
ALTER TABLE "author" ALTER "name" SET DATA TYPE text USING $pga$unnamed author$pga$, ALTER "name" SET NOT NULL;`,
|
|
103
|
+
})
|
|
@@ -205,3 +205,23 @@ UPDATE "author" SET "name" = $pga$unnamed$pga$ WHERE "name" IS NULL;
|
|
|
205
205
|
SET CONSTRAINTS ALL IMMEDIATE; SET CONSTRAINTS ALL DEFERRED;
|
|
206
206
|
ALTER TABLE "author" ALTER "name" SET NOT NULL;`,
|
|
207
207
|
})
|
|
208
|
+
|
|
209
|
+
testMigrations('set not null and fill with "using"', {
|
|
210
|
+
original: createSchema(NotNullOrig),
|
|
211
|
+
updated: createSchema(NotNullUpdated),
|
|
212
|
+
noDiff: true,
|
|
213
|
+
diff: [
|
|
214
|
+
updateColumnDefinitionModification.createModification({
|
|
215
|
+
entityName: 'Author',
|
|
216
|
+
fieldName: 'name',
|
|
217
|
+
definition: {
|
|
218
|
+
type: Model.ColumnType.String,
|
|
219
|
+
columnType: 'text',
|
|
220
|
+
nullable: false,
|
|
221
|
+
},
|
|
222
|
+
fillValue: 'unnamed',
|
|
223
|
+
valueMigrationStrategy: 'using',
|
|
224
|
+
}),
|
|
225
|
+
],
|
|
226
|
+
sql: SQL` ALTER TABLE "author" ALTER "name" SET DATA TYPE text USING COALESCE("name"::text, $pga$unnamed$pga$), ALTER "name" SET NOT NULL;`,
|
|
227
|
+
})
|