@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.
Files changed (24) hide show
  1. package/dist/src/modifications/columns/CreateColumnModification.d.ts +1 -0
  2. package/dist/src/modifications/columns/CreateColumnModification.d.ts.map +1 -1
  3. package/dist/src/modifications/columns/CreateColumnModification.js +27 -15
  4. package/dist/src/modifications/columns/CreateColumnModification.js.map +1 -1
  5. package/dist/src/modifications/columns/UpdateColumnDefinitionModification.d.ts +1 -0
  6. package/dist/src/modifications/columns/UpdateColumnDefinitionModification.d.ts.map +1 -1
  7. package/dist/src/modifications/columns/UpdateColumnDefinitionModification.js +27 -13
  8. package/dist/src/modifications/columns/UpdateColumnDefinitionModification.js.map +1 -1
  9. package/dist/src/modifications/columns/columnUtils.d.ts +9 -5
  10. package/dist/src/modifications/columns/columnUtils.d.ts.map +1 -1
  11. package/dist/src/modifications/columns/columnUtils.js +15 -15
  12. package/dist/src/modifications/columns/columnUtils.js.map +1 -1
  13. package/dist/src/tsconfig.tsbuildinfo +1 -1
  14. package/dist/tests/cases/integration/createColumn.test.js +37 -2
  15. package/dist/tests/cases/integration/createColumn.test.js.map +1 -1
  16. package/dist/tests/cases/integration/updateColumnDefinition.test.js +19 -0
  17. package/dist/tests/cases/integration/updateColumnDefinition.test.js.map +1 -1
  18. package/dist/tests/tsconfig.tsbuildinfo +1 -1
  19. package/package.json +7 -7
  20. package/src/modifications/columns/CreateColumnModification.ts +32 -16
  21. package/src/modifications/columns/UpdateColumnDefinitionModification.ts +30 -14
  22. package/src/modifications/columns/columnUtils.ts +22 -16
  23. package/tests/cases/integration/createColumn.test.ts +35 -2
  24. 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.2",
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.2",
16
- "@contember/database-migrations": "1.4.0-rc.2",
17
- "@contember/engine-common": "1.4.0-rc.2",
18
- "@contember/schema": "1.4.0-rc.2",
19
- "@contember/schema-utils": "1.4.0-rc.2",
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.2"
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
- const hasSeed = this.data.fillValue !== undefined || this.data.copyValue !== undefined
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 && !hasSeed,
34
+ notNull: !column.nullable && seedExpression === null,
25
35
  sequenceGenerated: column.sequence,
26
36
  },
27
37
  })
28
38
 
29
- if (hasSeed) {
30
- fillSeed({
31
- builder,
32
- type: 'creating',
33
- model: this.schema.model,
34
- entity,
35
- columnName: column.columnName,
36
- nullable: column.nullable,
37
- columnType,
38
- copyValue: this.data.copyValue,
39
- fillValue: this.data.fillValue,
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 hasSeed = this.data.fillValue !== undefined || this.data.copyValue !== undefined
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: oldColumn.nullable !== newColumn.nullable && (!hasSeed || newColumn.nullable) ? !newColumn.nullable : undefined,
33
- using: hasNewSequence
34
- ? `COALESCE(${usingCast}, nextval(PG_GET_SERIAL_SEQUENCE(${escapeValue(entity.tableName)}, ${escapeValue(oldColumn.columnName)})))`
35
- : hasNewType
36
- ? usingCast
37
- : undefined,
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 (hasSeed) {
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
- columnType,
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, fillValue, copyValue, entity, columnName, nullable, model, columnType, type }: {
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
- if (fillValue !== undefined) {
20
- builder.sql(`UPDATE ${wrapIdentifier(entity.tableName)}
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().default('unnamed author').notNull()
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
+ })