-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathdrift_enums.go
263 lines (224 loc) · 7.55 KB
/
drift_enums.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
package pgutil
import (
"fmt"
"strings"
)
type EnumModifier struct {
s SchemaDescription
d EnumDescription
}
func NewEnumModifier(s SchemaDescription, d EnumDescription) EnumModifier {
return EnumModifier{
s: s,
d: d,
}
}
func (m EnumModifier) Key() string {
return fmt.Sprintf("%q.%q", m.d.Namespace, m.d.Name)
}
func (m EnumModifier) ObjectType() string {
return "enum"
}
func (m EnumModifier) Description() EnumDescription {
return m.d
}
func (m EnumModifier) Create() string {
var quotedLabels []string
for _, label := range m.d.Labels {
quotedLabels = append(quotedLabels, enumQuote(label))
}
return fmt.Sprintf("CREATE TYPE %s AS ENUM (%s);", m.Key(), strings.Join(quotedLabels, ", "))
}
func (m EnumModifier) Drop() string {
return fmt.Sprintf("DROP TYPE IF EXISTS %s;", m.Key())
}
// NOTE: This depends on the order of the schema being modified. We must be certain that the order of
// drop/apply/create ensures that the columns here in the existing schema are still valid within the
// schema being altered.
func (m EnumModifier) AlterExisting(existingSchema SchemaDescription, existingObject EnumDescription) ([]ddlStatement, bool) {
if reconstruction, ok := unifyLabels(m.d.Labels, existingObject.Labels); ok {
return m.alterViaReconstruction(reconstruction)
}
return m.alterViaDropAndRecreate(existingSchema)
}
func (m EnumModifier) alterViaReconstruction(reconstruction []missingLabel) ([]ddlStatement, bool) {
var statements []string
for _, missingLabel := range reconstruction {
relativeTo := ""
if missingLabel.Next != nil {
relativeTo = fmt.Sprintf("BEFORE %s", enumQuote(*missingLabel.Next))
} else {
relativeTo = fmt.Sprintf("AFTER %s", enumQuote(*missingLabel.Prev))
}
statements = append(statements, fmt.Sprintf("ALTER TYPE %q.%q ADD VALUE %s %s;", m.d.Namespace, m.d.Name, enumQuote(missingLabel.Label), relativeTo))
}
return []ddlStatement{
newStatement(
m.Key(),
"replace",
m.ObjectType(),
statements...,
),
}, true
}
func (m EnumModifier) alterViaDropAndRecreate(existingSchema SchemaDescription) ([]ddlStatement, bool) {
// Basic plan:
// 1. Rename the existing enum type.
// 2. Create the new enum type with the old name.
// 3. Drop all views that depend (transitively) on the enum type.
// 4. Drop defaults on columns that reference the enum type.
// 5. Alter column types to reference the new enum type.
// 6. Re-add any defaults that were dropped.
// 7. Recreate the views that were dropped.
// 8. Drop the old enum type.
//
// NOTE: View statements are ordered by `Compare`. We do, however, need to be cautious
// of the order in which we modify the enums and tables.
// Select the dependencies that are relevant to the enum type we're modifying.
var dependencies []EnumDependency
for _, dependency := range existingSchema.EnumDependencies {
if dependency.EnumNamespace == m.d.Namespace && dependency.EnumName == m.d.Name {
dependencies = append(dependencies, dependency)
}
}
// Calculate the transitive dependencies for all views in the current schema.
createDependencyClosure, _ := viewDependencyClosures(existingSchema, SchemaDescription{})
// Collect the set of views referencing a table with a column of the enum type.
var views []string
for _, dependency := range dependencies {
for key := range createDependencyClosure[fmt.Sprintf("%q.%q", dependency.TableNamespace, dependency.TableName)] {
views = append(views, key)
}
}
// Generate ALTER TABLE statements for each table with a column of the enum type.
var alterTableStatements []string
for _, dependency := range dependencies {
defaultValue := getDefaultValue(
existingSchema.Tables,
dependency.EnumNamespace,
dependency.TableName,
dependency.ColumnName,
)
var alterTableActions []string
if defaultValue != "" {
alterTableActions = append(alterTableActions, fmt.Sprintf(
"ALTER COLUMN %q DROP DEFAULT",
dependency.ColumnName,
))
}
alterTableActions = append(alterTableActions, fmt.Sprintf(
"ALTER COLUMN %q TYPE %s USING (%q::text::%s)",
dependency.ColumnName,
m.Key(),
dependency.ColumnName,
m.Key(),
))
if defaultValue != "" {
alterTableActions = append(alterTableActions, fmt.Sprintf(
"ALTER COLUMN %q SET DEFAULT %s",
dependency.ColumnName,
defaultValue,
))
}
alterTableStatements = append(alterTableStatements, fmt.Sprintf(
"ALTER TABLE %q.%q %s;",
dependency.TableNamespace,
dependency.TableName,
strings.Join(alterTableActions, ", "),
))
}
// Generate DROP/CREATE VIEW statements for each view that references the enum type.
var viewStatements []ddlStatement
for _, viewKey := range views {
viewStatements = append(viewStatements, newStatement(
viewKey,
"drop",
"view",
fmt.Sprintf("DROP VIEW IF EXISTS %s;", viewKey),
))
// Look for the view in the new schema (which may have been dropped or modified). If the view
// exists and has the SAME definition, then we need to be sure to issue a recreation statement,
// otherwise we've dropped the view as an unintentional side-effect. If the view exists and has
// a different definition, then we don't need to recreate the view because it will be recreated
// as part of the normal view drift repair.
var existingDefinition string
for _, view := range existingSchema.Views {
if fmt.Sprintf("%q.%q", view.Namespace, view.Name) == viewKey {
existingDefinition = view.Definition
}
}
for _, view := range m.s.Views {
if viewKey == fmt.Sprintf("%q.%q", view.Namespace, view.Name) && view.Definition == existingDefinition {
viewStatements = append(viewStatements, newStatement(
viewKey,
"create",
"view",
fmt.Sprintf("CREATE OR REPLACE VIEW %s AS %s", viewKey, strings.TrimSpace(stripIdent(" "+existingDefinition))),
))
}
}
}
// Construct enum replacement statements.
var enumStatements []string
enumStatements = append(enumStatements, fmt.Sprintf("ALTER TYPE %q.%q RENAME TO %q;", m.d.Namespace, m.d.Name, m.d.Name+"_bak"))
enumStatements = append(enumStatements, m.Create())
enumStatements = append(enumStatements, alterTableStatements...)
enumStatements = append(enumStatements, fmt.Sprintf("DROP TYPE %q.%q;", m.d.Namespace, m.d.Name+"_bak"))
return append(viewStatements, newStatement(
m.Key(),
"replace",
m.ObjectType(),
enumStatements...,
)), true
}
func enumQuote(label string) string {
return fmt.Sprintf("'%s'", strings.ReplaceAll(label, "'", "''"))
}
func getDefaultValue(tables []TableDescription, namespace, tableName, columnName string) string {
for _, table := range tables {
if table.Namespace == namespace && table.Name == tableName {
for _, c := range table.Columns {
if c.Name == columnName {
return c.Default
}
}
}
}
return ""
}
type missingLabel struct {
Label string
Prev *string
Next *string
}
func unifyLabels(expectedLabels, existingLabels []string) (reconstruction []missingLabel, _ bool) {
var (
j = 0
missingIndexMap = map[int]struct{}{}
)
for i, label := range expectedLabels {
if j < len(existingLabels) && existingLabels[j] == label {
j++
} else if i > 0 {
missingIndexMap[i] = struct{}{}
}
}
if j < len(existingLabels) {
return nil, false
}
if expectedLabels[0] != existingLabels[0] {
reconstruction = append(reconstruction, missingLabel{
Label: expectedLabels[0],
Next: &existingLabels[0],
})
}
for i, label := range expectedLabels {
if _, ok := missingIndexMap[i]; ok {
reconstruction = append(reconstruction, missingLabel{
Label: label,
Prev: &expectedLabels[i-1],
})
}
}
return reconstruction, true
}