Partitioning: fix handling of several edge cases

This commit fixes support for several relatively rare partitioning clauses:

* HASH or KEY partitioned tables with a single partition, created by omitting
  both the partition list and count, were previously unsupported for diff.
  Now handled correctly. Fixes #111.

* HASH or KEY partitioned tables without individual partition comments, data
  directories, or nonstandard naming, were previously unsupported for diff if
  they were created using an explicit list of the partitions (rather than a
  count). Now handled correctly.

* DATA DIRECTORY clauses are now properly parsed and retained.

* KEY partitioned tables with ALGORITHM clause is now properly parsed and
  retained.
This commit is contained in:
Evan Elias 2019-11-19 01:31:31 -05:00
Родитель aac8a060e5
Коммит d2ae6af5de
8 изменённых файлов: 100 добавлений и 59 удалений

Просмотреть файл

@ -95,12 +95,12 @@ The following object types are completely ignored by Skeema. Their presence won'
Skeema can CREATE or DROP tables using these features, but cannot ALTER them. The output of `skeema diff` and `skeema push` will note that it cannot generate or run ALTER TABLE for tables using these features, so the affected table(s) will be skipped, but the rest of the operation will proceed as normal.
* generated/virtual columns (MySQL 5.7+ / Percona Server 5.7+ / MariaDB 5.2+)
* spatial column types
* sub-partitioning (two levels of partitioning in the same table)
* CHECK constraints (MySQL 8.0.16+ / Percona Server 8.0.16+ / MariaDB 10.2+)
* column-level compression, with or without predefined dictionary (Percona Server 5.6.33+)
* some features of non-InnoDB storage engines
* spatial column types
* generated/virtual columns
* column-level compression, with or without predefined dictionary (Percona Server 5.6.33+)
* CHECK constraints (MySQL 8.0.16+ / Percona Server 8.0.16+ / MariaDB 10.2+)
You can still ALTER these tables externally from Skeema (e.g., direct invocation of `ALTER TABLE` or `pt-online-schema-change`). Afterwards, you can update your schema repo using `skeema pull`, which will work properly even on these tables.
@ -136,12 +136,9 @@ Skeema v1.4.0 added support for partitioned tables. The diff/push functionality
Skeema intentionally ignores changes to the *list of partitions* for an already-partitioned table using RANGE or LIST partitioning methods; the assumption is that an external partition management script/cron is responsible for handling this, outside of the scope of the schema repository. Meanwhile, for HASH or KEY partitioning methods, attempting to change the partition count causes an unsupported diff error, skipping the affected table. Future versions of Skeema may add additional options controlling these behaviors.
Whenever a partitioned table is being dropped, Skeema will generate a series of `ALTER TABLE ... DROP PARTITION` clauses to drop all but 1 partition prior to generating the `DROP TABLE`. This avoids having a single excessively-long `DROP TABLE` operation, which could be disruptive to other queries since it holds MySQL's dict_sys mutex.
Whenever a RANGE or LIST partitioned table is being dropped, Skeema will generate a series of `ALTER TABLE ... DROP PARTITION` clauses to drop all but 1 partition prior to generating the `DROP TABLE`. This avoids having a single excessively-long `DROP TABLE` operation, which could be disruptive to other queries since it holds MySQL's dict_sys mutex.
Sub-partitioning (two levels of partitioning in the same table) is not supported for diff operations yet, as this feature adds complexity and is infrequently used.
Two known issues are planned to be patched in an upcoming release:
* Skeema currently ignores `DATA DIRECTORY` clauses and `ALGORITHM = 1` clauses in partitioned tables. When creating a new table via Skeema, these clauses may be unintentionally stripped from the DDL if present. The fix will retain these clauses as written.
* When running `skeema pull` against an environment that uses `partitioning=remove`, since the environment has no partitions, the *.sql files will be rewritten such that the `CREATE TABLE` statements lose their `PARTITION BY` clauses. By design this won't interfere with other environments that use `partitioning=keep`, however it is cosmetically undesirable and potentially confusing in SCM history.
When running `skeema pull` against an environment that uses `partitioning=remove`, please be aware that since the environment has no partitions, the *.sql files will be rewritten such that the `CREATE TABLE` statements lose their `PARTITION BY` clauses. By design this won't interfere with other environments that use `partitioning=keep`, however it is cosmetically undesirable and potentially confusing in SCM history.

2
go.mod
Просмотреть файл

@ -12,7 +12,7 @@ require (
github.com/opencontainers/runc v1.0.0-rc5 // indirect
github.com/sirupsen/logrus v1.4.2
github.com/skeema/mybase v1.0.8
github.com/skeema/tengo v0.8.20-0.20191023033155-f3604144981e
github.com/skeema/tengo v0.8.20-0.20191119054631-833a1bde9244
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4
golang.org/x/lint v0.0.0-20190409202823-959b441ac422
golang.org/x/sync v0.0.0-20190423024810-112230192c58

4
go.sum
Просмотреть файл

@ -68,8 +68,8 @@ github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/skeema/mybase v1.0.8 h1:eqtxi5FfphYhosEEeHBsYf/9oXfwjCZ8fMnpGJ+AdxI=
github.com/skeema/mybase v1.0.8/go.mod h1:09Uz3MIoXTNCUZWBeKDeO8SUHlQNjIEocXbc1DFvEKQ=
github.com/skeema/tengo v0.8.20-0.20191023033155-f3604144981e h1:RgNLLi6USC85MpI/mzs9u+V7wsWyQILTTEKF06aEWDY=
github.com/skeema/tengo v0.8.20-0.20191023033155-f3604144981e/go.mod h1:7ahmzzEKjeOzHEqq0okxccPFozsDyGmZORUfF25GRYc=
github.com/skeema/tengo v0.8.20-0.20191119054631-833a1bde9244 h1:rKjOLp/GJrNZ/h/Mtf+GSJ2TRm8ocacKSNbH/hy9EQQ=
github.com/skeema/tengo v0.8.20-0.20191119054631-833a1bde9244/go.mod h1:7ahmzzEKjeOzHEqq0okxccPFozsDyGmZORUfF25GRYc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=

47
vendor/github.com/skeema/tengo/instance.go сгенерированный поставляемый
Просмотреть файл

@ -1121,7 +1121,7 @@ func (instance *Instance) querySchemaTables(schema string) ([]*Table, error) {
t.CreateStatement = NormalizeCreateOptions(t.CreateStatement)
}
if t.Partitioning != nil {
t.CreateStatement = NormalizePartitioning(t.CreateStatement, flavor)
fixPartitioningEdgeCases(t, flavor)
}
// Index order is unpredictable with new MySQL 8 data dictionary, so reorder
// indexes based on parsing SHOW CREATE TABLE if needed
@ -1229,6 +1229,51 @@ func fixCreateOptionsOrder(t *Table, flavor Flavor) {
}
}
// fixPartitioningEdgeCases handles situations that are reflected in SHOW CREATE
// TABLE, but missing (or difficult to obtain) in information_schema.
func fixPartitioningEdgeCases(t *Table, flavor Flavor) {
// Handle edge cases for how partitions are expressed in HASH or KEY methods:
// typically this will just be a PARTITIONS N clause, but it could also be
// nothing at all, or an explicit list of partitions, depending on how the
// partitioning was originally created.
if strings.HasSuffix(t.Partitioning.Method, "HASH") || strings.HasSuffix(t.Partitioning.Method, "KEY") {
countClause := fmt.Sprintf("\nPARTITIONS %d", len(t.Partitioning.Partitions))
if strings.Contains(t.CreateStatement, countClause) {
t.Partitioning.forcePartitionList = partitionListCount
} else if strings.Contains(t.CreateStatement, "\n(PARTITION ") {
t.Partitioning.forcePartitionList = partitionListExplicit
} else if len(t.Partitioning.Partitions) == 1 {
t.Partitioning.forcePartitionList = partitionListNone
}
}
// KEY methods support an optional ALGORITHM clause, which is present in SHOW
// CREATE TABLE but not anywhere in information_schema
if strings.HasSuffix(t.Partitioning.Method, "KEY") && strings.Contains(t.CreateStatement, "ALGORITHM") {
re := regexp.MustCompile(fmt.Sprintf(`PARTITION BY %s ([^(]*)\(`, t.Partitioning.Method))
if matches := re.FindStringSubmatch(t.CreateStatement); matches != nil {
t.Partitioning.algoClause = matches[1]
}
}
// Process DATA DIRECTORY clauses, which are easier to parse from SHOW CREATE
// TABLE instead of information_schema.innodb_sys_tablespaces.
if (t.Partitioning.forcePartitionList == partitionListDefault || t.Partitioning.forcePartitionList == partitionListExplicit) &&
strings.Contains(t.CreateStatement, " DATA DIRECTORY = ") {
for _, p := range t.Partitioning.Partitions {
name := p.Name
if flavor.VendorMinVersion(VendorMariaDB, 10, 2) {
name = EscapeIdentifier(name)
}
name = regexp.QuoteMeta(name)
re := regexp.MustCompile(fmt.Sprintf(`PARTITION %s .*DATA DIRECTORY = '((?:\\\\|\\'|''|[^'])*)'`, name))
if matches := re.FindStringSubmatch(t.CreateStatement); matches != nil {
p.dataDir = matches[1]
}
}
}
}
func (instance *Instance) querySchemaRoutines(schema string) ([]*Routine, error) {
db, err := instance.Connect("information_schema", "")
if err != nil {

62
vendor/github.com/skeema/tengo/partition.go сгенерированный поставляемый
Просмотреть файл

@ -5,16 +5,29 @@ import (
"strings"
)
// partitionListMode enum values control edge-cases for how the list of
// partitions is represented in SHOW CREATE TABLE.
type partitionListMode int
const (
partitionListDefault partitionListMode = iota
partitionListExplicit // List each partition individually
partitionListCount // Just use a count of partitions
partitionListNone // Omit partition list and count, implying just 1 partition
)
// TablePartitioning stores partitioning configuration for a partitioned table.
// Note that despite subpartitioning fields being present and possibly
// populated, the rest of this package does not fully support subpartitioning
// yet.
type TablePartitioning struct {
Method string // one of "RANGE", "RANGE COLUMNS", "LIST", "LIST COLUMNS", "HASH", "LINEAR HASH", "KEY", or "LINEAR KEY"
SubMethod string // one of "" (no sub-partitioning), "HASH", "LINEAR HASH", "KEY", or "LINEAR KEY"; not fully supported yet
Expression string
SubExpression string // empty string if no sub-partitioning; not fully supported yet
Partitions []*Partition
Method string // one of "RANGE", "RANGE COLUMNS", "LIST", "LIST COLUMNS", "HASH", "LINEAR HASH", "KEY", or "LINEAR KEY"
SubMethod string // one of "" (no sub-partitioning), "HASH", "LINEAR HASH", "KEY", or "LINEAR KEY"; not fully supported yet
Expression string
SubExpression string // empty string if no sub-partitioning; not fully supported yet
Partitions []*Partition
forcePartitionList partitionListMode
algoClause string // full text of optional ALGORITHM clause for KEY or LINEAR KEY
}
// Definition returns the overall partitioning definition for a table.
@ -23,35 +36,38 @@ func (tp *TablePartitioning) Definition(flavor Flavor) string {
return ""
}
var needPartitionList bool
for n, p := range tp.Partitions {
if p.Values != "" || p.Comment != "" || p.Name != fmt.Sprintf("p%d", n) {
needPartitionList = true
break
plMode := tp.forcePartitionList
if plMode == partitionListDefault {
plMode = partitionListCount
for n, p := range tp.Partitions {
if p.Values != "" || p.Comment != "" || p.dataDir != "" || p.Name != fmt.Sprintf("p%d", n) {
plMode = partitionListExplicit
break
}
}
}
var partitionsClause string
if needPartitionList {
if plMode == partitionListExplicit {
pdefs := make([]string, len(tp.Partitions))
for n, p := range tp.Partitions {
pdefs[n] = p.Definition(flavor)
}
partitionsClause = fmt.Sprintf("(%s)", strings.Join(pdefs, ",\n "))
} else {
partitionsClause = fmt.Sprintf("PARTITIONS %d", len(tp.Partitions))
partitionsClause = fmt.Sprintf("\n(%s)", strings.Join(pdefs, ",\n "))
} else if plMode == partitionListCount {
partitionsClause = fmt.Sprintf("\nPARTITIONS %d", len(tp.Partitions))
}
open, close := "/*!50100", " */"
opener, closer := "/*!50100", " */"
if flavor.VendorMinVersion(VendorMariaDB, 10, 2) {
// MariaDB stopped wrapping partitioning clauses in version-gated comments
// in 10.2.
open, close = "", ""
opener, closer = "", ""
} else if strings.HasSuffix(tp.Method, "COLUMNS") {
// RANGE COLUMNS and LIST COLUMNS were introduced in 5.5
open = "/*!50500"
opener = "/*!50500"
}
return fmt.Sprintf("\n%s PARTITION BY %s\n%s%s", open, tp.partitionBy(flavor), partitionsClause, close)
return fmt.Sprintf("\n%s PARTITION BY %s%s%s", opener, tp.partitionBy(flavor), partitionsClause, closer)
}
// partitionBy returns the partitioning method and expression, formatted to
@ -69,7 +85,7 @@ func (tp *TablePartitioning) partitionBy(flavor Flavor) string {
expr = strings.Replace(expr, "`", "", -1)
}
return fmt.Sprintf("%s(%s)", method, expr)
return fmt.Sprintf("%s%s(%s)", method, tp.algoClause, expr)
}
// Diff returns a set of differences between this TablePartitioning and another
@ -127,6 +143,7 @@ type Partition struct {
Comment string
method string
engine string
dataDir string
}
// Definition returns this partition's definition clause, for use as part of a
@ -146,10 +163,15 @@ func (p *Partition) Definition(flavor Flavor) string {
values = fmt.Sprintf("VALUES IN (%s) ", p.Values)
}
var dataDir string
if p.dataDir != "" {
dataDir = fmt.Sprintf("DATA DIRECTORY = '%s' ", p.dataDir) // any necessary escaping is already present in p.dataDir
}
var comment string
if p.Comment != "" {
comment = fmt.Sprintf("COMMENT = '%s' ", EscapeValueForCreateTable(p.Comment))
}
return fmt.Sprintf("PARTITION %s %s%sENGINE = %s", name, values, comment, p.engine)
return fmt.Sprintf("PARTITION %s %s%s%sENGINE = %s", name, values, dataDir, comment, p.engine)
}

4
vendor/github.com/skeema/tengo/table.go сгенерированный поставляемый
Просмотреть файл

@ -93,8 +93,8 @@ func (t *Table) UnpartitionedCreateStatement(flavor Flavor) string {
}
// If our generated partitioning clause definition isn't 100% aligned with
// SHOW CREATE TABLE (due to unsupported features or due to adjustments made
// in NormalizePartitioning), just search for just the beginning of the clause.
// SHOW CREATE TABLE (e.g. due to unsupported features), just search for just
// the beginning of the clause.
partClause := t.Partitioning.Definition(flavor)
if t.UnsupportedDDL || !strings.Contains(t.CreateStatement, partClause) {
headerPos := strings.Index(partClause, " PARTITION BY ")

23
vendor/github.com/skeema/tengo/util.go сгенерированный поставляемый
Просмотреть файл

@ -139,29 +139,6 @@ func NormalizeCreateOptions(createStmt string) string {
return createStmt
}
var reDataDirectory = regexp.MustCompile(` DATA DIRECTORY = '(\\\\|\\'|''|[^'])*'`)
// NormalizePartitioning adjusts the supplied CREATE TABLE's partitioning clause
// to remove any oddities that are not reflected in information_schema and/or
// cannot be altered by this package, but are included in SHOW CREATE TABLE.
func NormalizePartitioning(createStmt string, flavor Flavor) string {
if flavor.Major == 5 && flavor.Minor == 5 {
createStmt = strings.Replace(createStmt, "PARTITION BY KEY */ /*!50531 ALGORITHM = 1 */ /*!50100 ", "PARTITION BY KEY ", 1)
} else if flavor.MySQLishMinVersion(5, 6) || flavor == FlavorMariaDB101 {
createStmt = strings.Replace(createStmt, "PARTITION BY KEY */ /*!50611 ALGORITHM = 1 */ /*!50100 ", "PARTITION BY KEY ", 1)
} else if flavor.VendorMinVersion(VendorMariaDB, 10, 2) {
createStmt = strings.Replace(createStmt, "PARTITION BY KEY ALGORITHM = 1 ", "PARTITION BY KEY ", 1)
}
// Ignore DATA DIRECTORY clauses for now. These only affect the partition list
// (which this package does not diff/alter yet), so no sense in doing extra
// queries to introspect them, which would require looking in nonstandard
// information_schema tables innodb_sys_datafiles and innodb_sys_tables.
createStmt = reDataDirectory.ReplaceAllString(createStmt, "")
return createStmt
}
// baseDSN returns a DSN with the database (schema) name and params stripped.
// Currently only supports MySQL, via go-sql-driver/mysql's DSN format.
func baseDSN(dsn string) string {

2
vendor/modules.txt поставляемый
Просмотреть файл

@ -75,7 +75,7 @@ github.com/pmezard/go-difflib/difflib
github.com/sirupsen/logrus
# github.com/skeema/mybase v1.0.8
github.com/skeema/mybase
# github.com/skeema/tengo v0.8.20-0.20191023033155-f3604144981e
# github.com/skeema/tengo v0.8.20-0.20191119054631-833a1bde9244
github.com/skeema/tengo
# golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4
golang.org/x/crypto/ssh/terminal