Skip to content

Commit aa72725

Browse files
authored
Support null-terminated strings in MySQL DDL (#4459)
Trailing `\x00` is the most common case of parse failures and also the most silly one. Closes DBI-859
1 parent 624d450 commit aa72725

2 files changed

Lines changed: 47 additions & 1 deletion

File tree

flow/connectors/mysql/cdc.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package connmysql
22

33
import (
4+
"bytes"
45
"cmp"
56
"context"
67
"crypto/tls"
@@ -46,6 +47,12 @@ func (c *MySqlConnector) binlogStalenessThreshold() time.Duration {
4647
return binlogStalenessMultiplier * c.binlogHeartbeatPeriod
4748
}
4849

50+
func parseSQL(parser *parser.Parser, query []byte) ([]ast.StmtNode, []error, error) {
51+
// TIDB parser errors on null-terminated strings
52+
trimmedQuery := shared.UnsafeFastReadOnlyBytesToString(bytes.TrimRight(query, "\x00"))
53+
return parser.ParseSQL(trimmedQuery)
54+
}
55+
4956
func (c *MySqlConnector) GetTableSchema(
5057
ctx context.Context,
5158
env map[string]string,
@@ -571,7 +578,7 @@ func (c *MySqlConnector) PullRecords(
571578
if mysqlParser == nil {
572579
mysqlParser = parser.New()
573580
}
574-
stmts, warns, err := mysqlParser.ParseSQL(shared.UnsafeFastReadOnlyBytesToString(ev.Query))
581+
stmts, warns, err := parseSQL(mysqlParser, ev.Query)
575582
if err != nil {
576583
c.logger.Warn("failed to parse QueryEvent", slog.String("query", string(ev.Query)), slog.Any("error", err))
577584
break

flow/connectors/mysql/cdc_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"fmt"
66
"testing"
77

8+
"github.com/pingcap/tidb/pkg/parser"
9+
"github.com/pingcap/tidb/pkg/parser/ast"
810
"github.com/stretchr/testify/require"
911

1012
"github.com/PeerDB-io/peerdb/flow/generated/protos"
@@ -50,6 +52,43 @@ func createTestDB(t *testing.T, ctx context.Context, c *MySqlConnector, dbName s
5052
})
5153
}
5254

55+
func TestParseSQLParsesTrailingNull(t *testing.T) {
56+
for _, tc := range []struct {
57+
name string
58+
query []byte
59+
assert func(*testing.T, ast.StmtNode)
60+
}{
61+
{
62+
name: "alter table",
63+
query: []byte("ALTER TABLE t ADD COLUMN c INT\x00"),
64+
assert: func(t *testing.T, stmt ast.StmtNode) {
65+
t.Helper()
66+
require.IsType(t, &ast.AlterTableStmt{}, stmt)
67+
},
68+
},
69+
{
70+
name: "commit",
71+
query: []byte("COMMIT\x00"),
72+
assert: func(t *testing.T, stmt ast.StmtNode) {
73+
t.Helper()
74+
require.IsType(t, &ast.CommitStmt{}, stmt)
75+
},
76+
},
77+
} {
78+
t.Run(tc.name, func(t *testing.T) {
79+
mysqlParser := parser.New()
80+
_, _, err := mysqlParser.ParseSQL(string(tc.query))
81+
require.Error(t, err)
82+
83+
stmts, warns, err := parseSQL(mysqlParser, tc.query)
84+
require.NoError(t, err)
85+
require.Empty(t, warns)
86+
require.Len(t, stmts, 1)
87+
tc.assert(t, stmts[0])
88+
})
89+
}
90+
}
91+
5392
func TestGetTableSchemaCaseSensitiveIdentifiers(t *testing.T) {
5493
t.Parallel()
5594
ctx := t.Context()

0 commit comments

Comments
 (0)