/
table.go
268 lines (249 loc) · 9.24 KB
/
table.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
264
265
266
267
268
package tengo
import (
"fmt"
"regexp"
"strings"
)
// Table represents a single database table.
type Table struct {
Name string `json:"name"`
Engine string `json:"storageEngine"`
CharSet string `json:"defaultCharSet"`
Collation string `json:"defaultCollation"`
ShowCollation bool `json:"showCollation,omitempty"` // Include default COLLATE in SHOW CREATE TABLE: logic differs by flavor
CreateOptions string `json:"createOptions,omitempty"` // row_format, stats_persistent, stats_auto_recalc, etc
Columns []*Column `json:"columns"`
PrimaryKey *Index `json:"primaryKey,omitempty"`
SecondaryIndexes []*Index `json:"secondaryIndexes,omitempty"`
ForeignKeys []*ForeignKey `json:"foreignKeys,omitempty"`
Checks []*Check `json:"checks,omitempty"`
Comment string `json:"comment,omitempty"`
Tablespace string `json:"tablespace,omitempty"`
NextAutoIncrement uint64 `json:"nextAutoIncrement,omitempty"`
Partitioning *TablePartitioning `json:"partitioning,omitempty"` // nil if table isn't partitioned
UnsupportedDDL bool `json:"unsupportedForDiff,omitempty"` // If true, tengo cannot diff this table or auto-generate its CREATE TABLE
CreateStatement string `json:"showCreateTable"` // complete SHOW CREATE TABLE obtained from an instance
}
// ObjectKey returns a value useful for uniquely refering to a Table within a
// single Schema, for example as a map key.
func (t *Table) ObjectKey() ObjectKey {
if t == nil {
return ObjectKey{}
}
return ObjectKey{
Type: ObjectTypeTable,
Name: t.Name,
}
}
// Def returns the table's CREATE statement as a string.
func (t *Table) Def() string {
return t.CreateStatement
}
// AlterStatement returns the prefix to a SQL "ALTER TABLE" statement.
func (t *Table) AlterStatement() string {
return fmt.Sprintf("ALTER TABLE %s", EscapeIdentifier(t.Name))
}
// DropStatement returns a SQL statement that, if run, would drop this table.
func (t *Table) DropStatement() string {
return fmt.Sprintf("DROP TABLE %s", EscapeIdentifier(t.Name))
}
// GeneratedCreateStatement generates a CREATE TABLE statement based on the
// Table's Go field values. If t.UnsupportedDDL is false, this will match
// the output of MySQL's SHOW CREATE TABLE statement. But if t.UnsupportedDDL
// is true, this means the table uses MySQL features that Tengo does not yet
// support, and so the output of this method will differ from MySQL.
func (t *Table) GeneratedCreateStatement(flavor Flavor) string {
defs := make([]string, len(t.Columns), len(t.Columns)+len(t.SecondaryIndexes)+len(t.ForeignKeys)+len(t.Checks)+1)
for n, c := range t.Columns {
defs[n] = c.Definition(flavor)
}
if t.PrimaryKey != nil {
defs = append(defs, t.PrimaryKey.Definition(flavor))
}
for _, idx := range t.SecondaryIndexes {
defs = append(defs, idx.Definition(flavor))
}
for _, fk := range t.ForeignKeys {
defs = append(defs, fk.Definition(flavor))
}
for _, cc := range t.Checks {
defs = append(defs, cc.Definition(flavor))
}
var tablespaceClause string
if t.Tablespace != "" {
tablespaceClause = fmt.Sprintf(" /*!50100 TABLESPACE %s */", EscapeIdentifier(t.Tablespace))
}
var autoIncClause string
if t.NextAutoIncrement > 1 {
autoIncClause = fmt.Sprintf(" AUTO_INCREMENT=%d", t.NextAutoIncrement)
}
charSet := t.CharSet
// MySQL 8.0.24+ uses "utf8mb3" for table default charset in SHOW CREATE TABLE,
// but still "utf8" for cols there, and "utf8" everywhere in I_S
if charSet == "utf8" && flavor.MinMySQL(8, 0, 24) {
charSet = "utf8mb3"
}
var collate string
if t.ShowCollation {
collate = " COLLATE=" + t.Collation
}
var createOptions string
if t.CreateOptions != "" {
createOptions = " " + t.CreateOptions
}
var comment string
if t.Comment != "" {
comment = fmt.Sprintf(" COMMENT='%s'", EscapeValueForCreateTable(t.Comment))
}
result := fmt.Sprintf("CREATE TABLE %s (\n %s\n)%s ENGINE=%s%s DEFAULT CHARSET=%s%s%s%s%s",
EscapeIdentifier(t.Name),
strings.Join(defs, ",\n "),
tablespaceClause,
t.Engine,
autoIncClause,
charSet,
collate,
createOptions,
comment,
t.Partitioning.Definition(flavor),
)
return result
}
// UnpartitionedCreateStatement returns the table's CREATE statement without
// its PARTITION BY clause. Supplying an accurate flavor improves performance,
// but is not required; FlavorUnknown still works correctly.
func (t *Table) UnpartitionedCreateStatement(flavor Flavor) string {
if t.Partitioning == nil {
return t.CreateStatement
}
if partClause := t.Partitioning.Definition(flavor); strings.HasSuffix(t.CreateStatement, partClause) {
return t.CreateStatement[0 : len(t.CreateStatement)-len(partClause)]
}
base, _ := ParseCreatePartitioning(t.CreateStatement)
return base
}
// ColumnsByName returns a mapping of column names to Column value pointers,
// for all columns in the table.
func (t *Table) ColumnsByName() map[string]*Column {
result := make(map[string]*Column, len(t.Columns))
for _, c := range t.Columns {
result[c.Name] = c
}
return result
}
// SecondaryIndexesByName returns a mapping of index names to Index value
// pointers, for all secondary indexes in the table.
func (t *Table) SecondaryIndexesByName() map[string]*Index {
result := make(map[string]*Index, len(t.SecondaryIndexes))
for _, idx := range t.SecondaryIndexes {
result[idx.Name] = idx
}
return result
}
// foreignKeysByName returns a mapping of foreign key names to ForeignKey value
// pointers, for all foreign keys in the table.
func (t *Table) foreignKeysByName() map[string]*ForeignKey {
result := make(map[string]*ForeignKey, len(t.ForeignKeys))
for _, fk := range t.ForeignKeys {
result[fk.Name] = fk
}
return result
}
// checksByName returns a mapping of check constraint names to Check value
// pointers, for all check constraints in the table.
func (t *Table) checksByName() map[string]*Check {
result := make(map[string]*Check, len(t.Checks))
for _, cc := range t.Checks {
result[cc.Name] = cc
}
return result
}
// HasAutoIncrement returns true if the table contains an auto-increment column,
// or false otherwise.
func (t *Table) HasAutoIncrement() bool {
for _, c := range t.Columns {
if c.AutoIncrement {
return true
}
}
return false
}
// ClusteredIndexKey returns which index is used for an InnoDB table's clustered
// index. This will be the primary key if one exists; otherwise, it will be the
// first unique key made of only non-nullable, non-expression columns. If there
// is no such key, or if the table's engine isn't InnoDB, this method returns
// nil.
func (t *Table) ClusteredIndexKey() *Index {
if t.Engine != "InnoDB" {
return nil
}
if t.PrimaryKey != nil {
return t.PrimaryKey
}
cols := t.ColumnsByName()
nullable := func(index *Index) bool {
for _, part := range index.Parts {
if col := cols[part.ColumnName]; col == nil || col.Nullable {
return true
}
}
return false
}
for _, index := range t.SecondaryIndexes {
if index.Unique && !index.Functional() && !nullable(index) {
return index
}
}
return nil
}
var reTableRowFormatClause = regexp.MustCompile(`ROW_FORMAT=(\w+)`)
// RowFormatClause returns the table's ROW_FORMAT clause, if one was explicitly
// specified in the table's creation options. If no ROW_FORMAT clause was
// specified, but a KEY_BLOCK_SIZE is, "COMPRESSED" will be returned since MySQL
// applies this automatically. If no ROW_FORMAT or KEY_BLOCK_SIZE was specified,
// a blank string is returned.
// This method does not query an instance to determine if the table's actual
// ROW_FORMAT differs from what was requested in creation options; nor does it
// query the default row format if none was specified.
func (t *Table) RowFormatClause() string {
matches := reTableRowFormatClause.FindStringSubmatch(t.CreateOptions)
if matches != nil {
return matches[1]
}
if strings.Contains(t.CreateOptions, "KEY_BLOCK_SIZE") {
return "COMPRESSED"
}
return ""
}
// UniqueConstraintsWithColumn returns a slice of Indexes which have uniqueness
// constraints (primary key or unique secondary index) and include col as one
// of the index parts. If col is not part of any uniqueness constraints, a nil
// slice is returned.
func (t *Table) UniqueConstraintsWithColumn(col *Column) []*Index {
var result []*Index
if t.PrimaryKey != nil && indexHasColumn(t.PrimaryKey, col) {
result = append(result, t.PrimaryKey)
}
for _, idx := range t.SecondaryIndexes {
if idx.Unique && indexHasColumn(idx, col) {
result = append(result, idx)
}
}
return result
}
func indexHasColumn(idx *Index, col *Column) bool {
for _, part := range idx.Parts {
if part.ColumnName == col.Name {
return true
}
}
return false
}
// Diff returns a set of differences between this table and another table. Some
// edge cases are not supported, such as sub-partitioning, spatial indexes,
// MariaDB application time periods, or various non-InnoDB table features; in
// this case, supported will be false and clauses MAY OR MAY NOT be empty. Any
// returned clauses in that case must be carefully verified for correctness.
func (t *Table) Diff(to *Table) (clauses []TableAlterClause, supported bool) {
return diffTables(t, to)
}