Skip to content

Commit d1b1130

Browse files
authored
use "as binary" conversion to join info schema tables to resolve any collation issues (#4406)
1 parent 716748a commit d1b1130

2 files changed

Lines changed: 72 additions & 26 deletions

File tree

flow/connectors/mysql/cdc.go

Lines changed: 20 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -81,32 +81,18 @@ func (c *MySqlConnector) getTableSchemaForTable(
8181
return nil, err
8282
}
8383

84-
pkSeqRs, err := c.Execute(ctx, fmt.Sprintf(`
85-
select column_name, seq_in_index
86-
from information_schema.statistics
87-
where table_schema = '%s' and table_name = '%s' and index_name = 'PRIMARY'`,
88-
mysql.Escape(qualifiedTable.Namespace), mysql.Escape(qualifiedTable.Table)))
89-
if err != nil {
90-
return nil, err
91-
}
92-
pkSeqMap := make(map[string]int64, pkSeqRs.RowNumber())
93-
for idx := range pkSeqRs.RowNumber() {
94-
name, err := pkSeqRs.GetString(idx, 0)
95-
if err != nil {
96-
return nil, err
97-
}
98-
seq, err := pkSeqRs.GetInt(idx, 1)
99-
if err != nil {
100-
return nil, err
101-
}
102-
pkSeqMap[name] = seq
103-
}
104-
84+
// CAST(... AS BINARY) forces a case-sensitive, collation-independent comparison on the join keys so it works with
85+
// lower_case_table_names=0. The LEFT JOIN leaves seq_in_index NULL for non-primary-key columns.
10586
rs, err := c.Execute(ctx, fmt.Sprintf(`
106-
select column_name, column_type, is_nullable, numeric_precision, numeric_scale
107-
from information_schema.columns
108-
where table_schema = '%s' and table_name = '%s'
109-
order by ordinal_position`,
87+
select c.column_name, c.column_type, c.is_nullable, c.numeric_precision, c.numeric_scale, s.seq_in_index
88+
from information_schema.columns c
89+
left join information_schema.statistics s
90+
on cast(s.table_schema as binary) = cast(c.table_schema as binary)
91+
and cast(s.table_name as binary) = cast(c.table_name as binary)
92+
and cast(s.column_name as binary) = cast(c.column_name as binary)
93+
and s.index_name = 'PRIMARY'
94+
where c.table_schema = '%s' and c.table_name = '%s'
95+
order by c.ordinal_position`,
11096
mysql.Escape(qualifiedTable.Namespace), mysql.Escape(qualifiedTable.Table)))
11197
if err != nil {
11298
return nil, err
@@ -161,7 +147,15 @@ func (c *MySqlConnector) getTableSchemaForTable(
161147
}
162148
columns = append(columns, column)
163149

164-
if seq, ok := pkSeqMap[columnName]; ok {
150+
seqIsNull, err := rs.IsNull(idx, 5)
151+
if err != nil {
152+
return nil, err
153+
}
154+
if !seqIsNull {
155+
seq, err := rs.GetInt(idx, 5)
156+
if err != nil {
157+
return nil, err
158+
}
165159
primaryEntries = append(primaryEntries, pkEntry{name: columnName, seqInIndex: seq})
166160
}
167161
}

flow/connectors/mysql/cdc_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,55 @@ func TestGetTableSchemaCaseSensitiveIdentifiers(t *testing.T) {
106106
assertSchema(schemas[ulTable], []string{"id"}, "id", "ul")
107107
assertSchema(schemas[uuTable], []string{"id"}, "id", "uu")
108108
}
109+
110+
func TestGetTableSchemaPrimaryKeyVariants(t *testing.T) {
111+
t.Parallel()
112+
ctx := t.Context()
113+
connector := newTestConnector(t, ctx)
114+
115+
dbName := "pk_variants_test"
116+
createTestDB(t, ctx, connector, dbName)
117+
118+
exec := func(sql string) {
119+
t.Helper()
120+
_, err := connector.Execute(ctx, sql)
121+
require.NoError(t, err)
122+
}
123+
124+
assertSchema := func(schema *protos.TableSchema, pk []string, cols ...string) {
125+
t.Helper()
126+
require.NotNil(t, schema)
127+
if len(pk) == 0 {
128+
require.Empty(t, schema.PrimaryKeyColumns)
129+
} else {
130+
require.Equal(t, pk, schema.PrimaryKeyColumns)
131+
}
132+
require.Len(t, schema.Columns, len(cols))
133+
for i, name := range cols {
134+
require.Equal(t, name, schema.Columns[i].Name)
135+
}
136+
}
137+
138+
compositeTable := dbName + ".composite_pk"
139+
noPKTable := dbName + ".no_pk"
140+
mixedCaseTable := dbName + ".mixed_case_pk"
141+
142+
// Composite PK whose key order (b, a) differs from column definition order (a, b, c),
143+
// exercising the seq_in_index sort.
144+
exec(fmt.Sprintf("CREATE TABLE %s (a INT, b INT, c INT, PRIMARY KEY (b, a))", compositeTable))
145+
// Table without a primary key, exercising the LEFT JOIN's NULL seq_in_index path.
146+
exec(fmt.Sprintf("CREATE TABLE %s (a INT, b TEXT)", noPKTable))
147+
// PK on a mixed-case column name, exercising the case-preserving column_name join.
148+
exec(fmt.Sprintf("CREATE TABLE %s (`MyId` INT PRIMARY KEY, val TEXT)", mixedCaseTable))
149+
150+
schemas, err := connector.GetTableSchema(ctx, nil, shared.InternalVersion_Latest, protos.TypeSystem_Q,
151+
[]*protos.TableMapping{
152+
{SourceTableIdentifier: compositeTable},
153+
{SourceTableIdentifier: noPKTable},
154+
{SourceTableIdentifier: mixedCaseTable},
155+
})
156+
require.NoError(t, err)
157+
assertSchema(schemas[compositeTable], []string{"b", "a"}, "a", "b", "c")
158+
assertSchema(schemas[noPKTable], nil, "a", "b")
159+
assertSchema(schemas[mixedCaseTable], []string{"MyId"}, "MyId", "val")
160+
}

0 commit comments

Comments
 (0)