Skip to content

Commit 67a6a87

Browse files
Zohaib Sibte Hassanclaude
andcommitted
Fix: Strip MySQL column COLLATE and COMMENT in CREATE TABLE transpilation
- Strip column-level COLLATE (e.g., utf8mb4_unicode_ci) - SQLite doesn't support these - Strip column-level COMMENT - not valid SQLite syntax - Strip column charset definitions - Make isIntegerType() case-insensitive to properly strip integer widths - Rule now always applies to CREATE TABLE to handle tables without KEY definitions This fixes LiteSpeed Cache and similar WordPress plugins that use MySQL-specific column options in their CREATE TABLE statements. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent d22ddcc commit 67a6a87

File tree

3 files changed

+104
-33
lines changed

3 files changed

+104
-33
lines changed

protocol/query/transform/create_table.go

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,12 @@ import (
66
"vitess.io/vitess/go/vt/sqlparser"
77
)
88

9-
// CreateTableRule extracts non-primary, non-unique KEY/INDEX definitions from CREATE TABLE
10-
// into separate CREATE INDEX statements.
11-
//
12-
// MySQL allows KEY/INDEX definitions inline in CREATE TABLE, but SQLite prefers
13-
// separate CREATE INDEX statements. This rule:
9+
// CreateTableRule transforms MySQL CREATE TABLE to SQLite-compatible form:
10+
// - Strips MySQL-specific column options (COLLATE, COMMENT)
11+
// - Strips MySQL-specific table options (ENGINE, CHARSET, COLLATE)
12+
// - Extracts non-primary, non-unique KEY/INDEX definitions into separate CREATE INDEX statements
1413
// - Keeps PRIMARY KEY definitions in the CREATE TABLE
1514
// - Keeps UNIQUE KEY definitions in the CREATE TABLE (converted to CONSTRAINT by serializer)
16-
// - Extracts regular KEY/INDEX definitions and returns them as separate CREATE INDEX statements
1715
// - Strips column length specifications: KEY idx (col(191)) → col
1816
// - Skips FULLTEXT/SPATIAL indexes (passed through to serializer)
1917
type CreateTableRule struct {
@@ -49,19 +47,27 @@ func (r *CreateTableRule) Transform(stmt sqlparser.Statement, params []interface
4947
}
5048
}
5149

52-
if len(indexesToExtract) == 0 {
53-
return nil, ErrRuleNotApplicable
54-
}
55-
5650
create.TableSpec.Indexes = remainingIndexes
5751

5852
// Clear MySQL-specific table options (ENGINE, CHARSET, COLLATE)
5953
create.TableSpec.Options = nil
6054

61-
// Strip display widths from integer types: INTEGER(20) → INTEGER
55+
// Process columns: strip MySQL-specific options
6256
for _, col := range create.TableSpec.Columns {
63-
if col.Type != nil && isIntegerType(col.Type.Type) {
64-
col.Type.Length = nil
57+
if col.Type != nil {
58+
// Strip display widths from integer types: INTEGER(20) → INTEGER
59+
if isIntegerType(col.Type.Type) {
60+
col.Type.Length = nil
61+
}
62+
// Strip MySQL-specific column options
63+
if col.Type.Options != nil {
64+
// Strip MySQL-specific COLLATE (SQLite only supports NOCASE, BINARY, RTRIM)
65+
col.Type.Options.Collate = ""
66+
// Strip MySQL-specific COMMENT (not supported in SQLite column definitions)
67+
col.Type.Options.Comment = nil
68+
}
69+
// Also strip charset (SQLite doesn't use MySQL charsets)
70+
col.Type.Charset = sqlparser.ColumnCharset{}
6571
}
6672
}
6773

protocol/query/transform/create_table_test.go

Lines changed: 84 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ func TestCreateTableRule_ExtractIndexes(t *testing.T) {
5656
email VARCHAR(100),
5757
UNIQUE KEY email_idx (email)
5858
)`,
59-
wantApplicable: false,
60-
wantStatementCount: 0,
59+
wantApplicable: true, // Rule always applies to CREATE TABLE (strips MySQL-specific options)
60+
wantStatementCount: 1, // Just the CREATE TABLE with UNIQUE KEY inline, no indexes extracted
6161
},
6262
{
6363
name: "extract regular KEY, keep UNIQUE KEY inline",
@@ -92,8 +92,8 @@ func TestCreateTableRule_ExtractIndexes(t *testing.T) {
9292
id INT,
9393
PRIMARY KEY (id)
9494
)`,
95-
wantApplicable: false,
96-
wantStatementCount: 0,
95+
wantApplicable: true, // Rule always applies to CREATE TABLE (strips MySQL-specific options)
96+
wantStatementCount: 1, // Just the CREATE TABLE, no indexes extracted
9797
},
9898
{
9999
name: "composite index",
@@ -119,8 +119,8 @@ func TestCreateTableRule_ExtractIndexes(t *testing.T) {
119119
{
120120
name: "no indexes",
121121
input: "CREATE TABLE t (id INT PRIMARY KEY, name VARCHAR(100))",
122-
wantApplicable: false,
123-
wantStatementCount: 0,
122+
wantApplicable: true, // Rule always applies to CREATE TABLE (strips MySQL-specific options)
123+
wantStatementCount: 1, // Just the CREATE TABLE, no indexes extracted
124124
},
125125
{
126126
name: "not a CREATE TABLE",
@@ -209,13 +209,14 @@ func TestCreateTableRule_MultipleTransforms(t *testing.T) {
209209
stmt2, _ := sqlparser.NewTestParser().Parse(input2)
210210
results2, err := rule.Transform(stmt2, nil, nil, "", &SQLiteSerializer{})
211211

212-
// UNIQUE KEY should not be extracted, so rule should not apply
213-
if err != ErrRuleNotApplicable {
214-
t.Fatalf("expected ErrRuleNotApplicable for UNIQUE KEY only table, got: %v", err)
212+
// Rule now always applies to CREATE TABLE (to strip MySQL-specific options)
213+
// UNIQUE KEY is kept inline, not extracted
214+
if err != nil {
215+
t.Fatalf("expected rule to apply, got: %v", err)
215216
}
216217

217-
if results2 != nil {
218-
t.Errorf("expected nil results for UNIQUE KEY only table, got %d statements", len(results2))
218+
if len(results2) != 1 {
219+
t.Errorf("expected 1 statement for UNIQUE KEY only table, got %d statements", len(results2))
219220
}
220221
}
221222

@@ -232,18 +233,18 @@ func TestCreateTableRule_UniqueConstraintFormat(t *testing.T) {
232233
}
233234

234235
rule := &CreateTableRule{}
235-
_, err = rule.Transform(stmt, nil, nil, "", &SQLiteSerializer{})
236+
results, err := rule.Transform(stmt, nil, nil, "", &SQLiteSerializer{})
236237

237-
// Should not be applicable since UNIQUE indexes are not extracted
238-
if err != ErrRuleNotApplicable {
239-
t.Fatalf("expected ErrRuleNotApplicable, got: %v", err)
238+
// Rule now always applies to CREATE TABLE (to strip MySQL-specific options)
239+
if err != nil {
240+
t.Fatalf("expected rule to apply, got: %v", err)
240241
}
241242

242-
// Now test that the serializer properly formats UNIQUE as CONSTRAINT
243-
create := stmt.(*sqlparser.CreateTable)
244-
serializer := &SQLiteSerializer{}
245-
sql := serializer.Serialize(create)
243+
if len(results) != 1 {
244+
t.Fatalf("expected 1 statement, got %d", len(results))
245+
}
246246

247+
sql := results[0].SQL
247248
// Should contain CONSTRAINT ... UNIQUE, not UNIQUE KEY
248249
lowerSQL := strings.ToLower(sql)
249250
if !strings.Contains(lowerSQL, "constraint") {
@@ -519,3 +520,67 @@ func TestCreateTableRule_CombinedOptionsAndWidths(t *testing.T) {
519520
t.Errorf("unexpected index statement: %q", results[1].SQL)
520521
}
521522
}
523+
524+
// TestCreateTableRule_StripColumnCollateAndComment tests that MySQL-specific
525+
// column options (COLLATE, COMMENT) are stripped from CREATE TABLE statements.
526+
func TestCreateTableRule_StripColumnCollateAndComment(t *testing.T) {
527+
input := `CREATE TABLE wp_litespeed_url_file (
528+
id bigint(20) NOT NULL AUTO_INCREMENT,
529+
vary varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT 'md5 of final vary',
530+
filename varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT 'md5 of file content',
531+
type tinyint(4) NOT NULL COMMENT 'css=1,js=2,ccss=3,ucss=4',
532+
PRIMARY KEY (id),
533+
KEY filename (filename),
534+
KEY type (type)
535+
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci`
536+
537+
stmt, err := sqlparser.NewTestParser().Parse(input)
538+
if err != nil {
539+
t.Fatalf("failed to parse SQL: %v", err)
540+
}
541+
542+
rule := &CreateTableRule{}
543+
results, err := rule.Transform(stmt, nil, nil, "", &SQLiteSerializer{})
544+
545+
if err != nil {
546+
t.Fatalf("Transform failed: %v", err)
547+
}
548+
549+
// Should have 3 statements: CREATE TABLE + 2 CREATE INDEX
550+
if len(results) != 3 {
551+
t.Fatalf("expected 3 statements, got %d", len(results))
552+
}
553+
554+
mainSQL := results[0].SQL
555+
lowerSQL := strings.ToLower(mainSQL)
556+
557+
// Check column-level COLLATE is stripped
558+
if strings.Contains(lowerSQL, "utf8mb4_unicode_ci") {
559+
t.Errorf("CREATE TABLE should not contain column COLLATE, got: %q", mainSQL)
560+
}
561+
562+
// Check COMMENT is stripped
563+
if strings.Contains(lowerSQL, "comment") {
564+
t.Errorf("CREATE TABLE should not contain COMMENT, got: %q", mainSQL)
565+
}
566+
567+
// Check table-level COLLATE is stripped
568+
if strings.Contains(lowerSQL, "utf8mb4_unicode_520_ci") {
569+
t.Errorf("CREATE TABLE should not contain table COLLATE, got: %q", mainSQL)
570+
}
571+
572+
// Check integer widths are stripped
573+
if strings.Contains(lowerSQL, "bigint(20)") || strings.Contains(lowerSQL, "tinyint(4)") {
574+
t.Errorf("CREATE TABLE should not contain integer widths, got: %q", mainSQL)
575+
}
576+
577+
// Check VARCHAR widths are preserved
578+
if !strings.Contains(lowerSQL, "varchar(32)") {
579+
t.Errorf("CREATE TABLE should preserve VARCHAR(32), got: %q", mainSQL)
580+
}
581+
582+
// Verify it's valid SQLite SQL (no MySQL-specific syntax)
583+
if strings.Contains(lowerSQL, "character set") {
584+
t.Errorf("CREATE TABLE should not contain CHARACTER SET, got: %q", mainSQL)
585+
}
586+
}

protocol/query/transform/int_type.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ func (r *IntTypeRule) Transform(stmt sqlparser.Statement, params []interface{},
9595

9696
// isIntegerType checks if the type is a MySQL integer type
9797
func isIntegerType(t string) bool {
98-
switch t {
98+
switch strings.ToUpper(t) {
9999
case "TINYINT", "SMALLINT", "MEDIUMINT", "INT", "INTEGER", "BIGINT":
100100
return true
101101
default:

0 commit comments

Comments
 (0)