Skip to content

Commit 7588372

Browse files
authored
feat: handle unsigned types (adbc-drivers#80)
adds handling for unsigned types: - for data queries, the issue was that go sql driver flips type names, it uses `UNSIGNED INT` instead of `INT UNSIGNED` and we have to flip it back. - for metadata queries, DataType column from information schema doesn't contain information about signed/unsigned. we have to get that additional information from ColumnType field.
1 parent ac9072c commit 7588372

File tree

4 files changed

+123
-5
lines changed

4 files changed

+123
-5
lines changed

go/connection.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ func (c *mysqlConnectionImpl) GetTableSchema(ctx context.Context, catalog *strin
8585
OrdinalPosition int32
8686
ColumnName string
8787
DataType string
88+
ColumnType string
8889
IsNullable string
8990
CharacterMaximumLength sql.NullInt64
9091
NumericPrecision sql.NullInt64
@@ -95,6 +96,7 @@ func (c *mysqlConnectionImpl) GetTableSchema(ctx context.Context, catalog *strin
9596
ORDINAL_POSITION,
9697
COLUMN_NAME,
9798
DATA_TYPE,
99+
COLUMN_TYPE,
98100
IS_NULLABLE,
99101
CHARACTER_MAXIMUM_LENGTH,
100102
NUMERIC_PRECISION,
@@ -132,6 +134,7 @@ func (c *mysqlConnectionImpl) GetTableSchema(ctx context.Context, catalog *strin
132134
&col.OrdinalPosition,
133135
&col.ColumnName,
134136
&col.DataType,
137+
&col.ColumnType,
135138
&col.IsNullable,
136139
&col.CharacterMaximumLength,
137140
&col.NumericPrecision,
@@ -166,9 +169,19 @@ func (c *mysqlConnectionImpl) GetTableSchema(ctx context.Context, catalog *strin
166169
scale = &col.NumericScale.Int64
167170
}
168171

172+
// Use DATA_TYPE but append UNSIGNED if COLUMN_TYPE indicates it
173+
// Only check integer types to avoid false positives with enum/set value lists
174+
dbTypeName := col.DataType
175+
switch strings.ToUpper(col.DataType) {
176+
case "TINYINT", "SMALLINT", "MEDIUMINT", "INT", "BIGINT":
177+
if strings.Contains(strings.ToUpper(col.ColumnType), "UNSIGNED") {
178+
dbTypeName = col.DataType + " UNSIGNED"
179+
}
180+
}
181+
169182
colType := sqlwrapper.ColumnType{
170183
Name: col.ColumnName,
171-
DatabaseTypeName: col.DataType,
184+
DatabaseTypeName: dbTypeName,
172185
Nullable: col.IsNullable == "YES",
173186
Length: length,
174187
Precision: precision,
@@ -328,6 +341,14 @@ func (c *mysqlConnectionImpl) arrowToMySQLType(arrowType arrow.DataType, nullabl
328341
mysqlType = "INT"
329342
case *arrow.Int64Type:
330343
mysqlType = "BIGINT"
344+
case *arrow.Uint8Type:
345+
mysqlType = "TINYINT UNSIGNED"
346+
case *arrow.Uint16Type:
347+
mysqlType = "SMALLINT UNSIGNED"
348+
case *arrow.Uint32Type:
349+
mysqlType = "INT UNSIGNED"
350+
case *arrow.Uint64Type:
351+
mysqlType = "BIGINT UNSIGNED"
331352
case *arrow.Float32Type:
332353
mysqlType = "FLOAT"
333354
case *arrow.Float64Type:

go/connection_getobjects.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ func (c *mysqlConnectionImpl) getTablesWithColumns(ctx context.Context, catalog
156156
ColumnName string
157157
ColumnComment sql.NullString
158158
DataType string
159+
ColumnType string
159160
IsNullable string
160161
ColumnDefault sql.NullString
161162
}
@@ -170,6 +171,7 @@ func (c *mysqlConnectionImpl) getTablesWithColumns(ctx context.Context, catalog
170171
c.COLUMN_NAME,
171172
c.COLUMN_COMMENT,
172173
c.DATA_TYPE,
174+
c.COLUMN_TYPE,
173175
c.IS_NULLABLE,
174176
c.COLUMN_DEFAULT
175177
FROM INFORMATION_SCHEMA.TABLES t
@@ -208,7 +210,7 @@ func (c *mysqlConnectionImpl) getTablesWithColumns(ctx context.Context, catalog
208210
if err := rows.Scan(
209211
&tc.TableName, &tc.TableType,
210212
&tc.OrdinalPosition, &tc.ColumnName, &tc.ColumnComment,
211-
&tc.DataType, &tc.IsNullable, &tc.ColumnDefault,
213+
&tc.DataType, &tc.ColumnType, &tc.IsNullable, &tc.ColumnDefault,
212214
); err != nil {
213215
return nil, c.ErrorHelper.WrapIO(err, "failed to scan table with columns")
214216
}
@@ -226,6 +228,16 @@ func (c *mysqlConnectionImpl) getTablesWithColumns(ctx context.Context, catalog
226228
var radix sql.NullInt16
227229
var nullable sql.NullInt16
228230

231+
// Build the full type name including UNSIGNED if applicable
232+
// Only check integer types to avoid false positives with enum/set value lists
233+
xdbcTypeName := tc.DataType
234+
switch strings.ToUpper(tc.DataType) {
235+
case "TINYINT", "SMALLINT", "MEDIUMINT", "INT", "BIGINT":
236+
if strings.Contains(strings.ToUpper(tc.ColumnType), "UNSIGNED") {
237+
xdbcTypeName = tc.DataType + " UNSIGNED"
238+
}
239+
}
240+
229241
// Set numeric precision radix (MySQL doesn't store this directly)
230242
dataType := strings.ToUpper(tc.DataType)
231243
switch dataType {
@@ -266,7 +278,7 @@ func (c *mysqlConnectionImpl) getTablesWithColumns(ctx context.Context, catalog
266278
ColumnName: tc.ColumnName,
267279
OrdinalPosition: &tc.OrdinalPosition,
268280
Remarks: driverbase.NullStringToPtr(tc.ColumnComment),
269-
XdbcTypeName: &tc.DataType,
281+
XdbcTypeName: &xdbcTypeName,
270282
XdbcNumPrecRadix: driverbase.NullInt16ToPtr(radix),
271283
XdbcNullable: driverbase.NullInt16ToPtr(nullable),
272284
XdbcIsNullable: &tc.IsNullable,

go/mysql.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,27 @@ type mySQLTypeConverter struct {
3838
sqlwrapper.DefaultTypeConverter
3939
}
4040

41+
// normalizeUnsignedTypeName converts "UNSIGNED INT" -> "INT UNSIGNED" format
42+
// The go-sql-driver/mysql returns "UNSIGNED X" but the default type converter expects "X UNSIGNED"
43+
func normalizeUnsignedTypeName(typeName string) string {
44+
if strings.HasPrefix(typeName, "UNSIGNED ") {
45+
return strings.TrimPrefix(typeName, "UNSIGNED ") + " UNSIGNED"
46+
}
47+
return typeName
48+
}
49+
4150
// ConvertRawColumnType implements TypeConverter with MySQL-specific enhancements
4251
func (m *mySQLTypeConverter) ConvertRawColumnType(colType sqlwrapper.ColumnType) (arrow.DataType, bool, arrow.Metadata, error) {
4352
typeName := strings.ToUpper(colType.DatabaseTypeName)
4453
nullable := colType.Nullable
4554

55+
// Normalize "UNSIGNED X" to "X UNSIGNED" for the default type converter
56+
// Only update DatabaseTypeName when reordering is needed, to preserve original casing in metadata
57+
typeName = normalizeUnsignedTypeName(typeName)
58+
if typeName != strings.ToUpper(colType.DatabaseTypeName) {
59+
colType.DatabaseTypeName = typeName
60+
}
61+
4662
switch typeName {
4763
case "BIT":
4864
// Handle BIT type as binary data

go/mysql_test.go

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,11 @@ func (s *MySQLTests) TestSelect() {
358358
point_col POINT,
359359
polygon_col POLYGON,
360360
geometry_col GEOMETRY,
361-
bit_col BIT(8)
361+
bit_col BIT(8),
362+
utinyint_col TINYINT UNSIGNED,
363+
usmallint_col SMALLINT UNSIGNED,
364+
uint_col INT UNSIGNED,
365+
ubigint_col BIGINT UNSIGNED
362366
)
363367
`))
364368
_, err := s.stmt.ExecuteUpdate(s.ctx)
@@ -372,7 +376,8 @@ func (s *MySQLTests) TestSelect() {
372376
ST_GeomFromText('POINT(1 2)'),
373377
ST_GeomFromText('POLYGON((0 0, 0 3, 3 3, 3 0, 0 0))'),
374378
ST_GeomFromText('LINESTRING(0 0, 1 1, 2 2)'),
375-
b'10101010'
379+
b'10101010',
380+
200, 60000, 3000000000, 10000000000000000000
376381
)
377382
`))
378383
_, err = s.stmt.ExecuteUpdate(s.ctx)
@@ -595,6 +600,70 @@ func (s *MySQLTests) TestSelect() {
595600
}, nil),
596601
expected: `[{"bitvalue": "qg=="}]`,
597602
},
603+
{
604+
name: "unsigned_tinyint",
605+
query: "SELECT utinyint_col AS value FROM test_types",
606+
schema: arrow.NewSchema([]arrow.Field{
607+
{
608+
Name: "value",
609+
Type: arrow.PrimitiveTypes.Uint8,
610+
Nullable: true,
611+
Metadata: arrow.MetadataFrom(map[string]string{
612+
"sql.column_name": "value",
613+
"sql.database_type_name": "TINYINT UNSIGNED",
614+
}),
615+
},
616+
}, nil),
617+
expected: `[{"value": 200}]`,
618+
},
619+
{
620+
name: "unsigned_smallint",
621+
query: "SELECT usmallint_col AS value FROM test_types",
622+
schema: arrow.NewSchema([]arrow.Field{
623+
{
624+
Name: "value",
625+
Type: arrow.PrimitiveTypes.Uint16,
626+
Nullable: true,
627+
Metadata: arrow.MetadataFrom(map[string]string{
628+
"sql.column_name": "value",
629+
"sql.database_type_name": "SMALLINT UNSIGNED",
630+
}),
631+
},
632+
}, nil),
633+
expected: `[{"value": 60000}]`,
634+
},
635+
{
636+
name: "unsigned_int",
637+
query: "SELECT uint_col AS value FROM test_types",
638+
schema: arrow.NewSchema([]arrow.Field{
639+
{
640+
Name: "value",
641+
Type: arrow.PrimitiveTypes.Uint32,
642+
Nullable: true,
643+
Metadata: arrow.MetadataFrom(map[string]string{
644+
"sql.column_name": "value",
645+
"sql.database_type_name": "INT UNSIGNED",
646+
}),
647+
},
648+
}, nil),
649+
expected: `[{"value": 3000000000}]`,
650+
},
651+
{
652+
name: "unsigned_bigint",
653+
query: "SELECT ubigint_col AS value FROM test_types",
654+
schema: arrow.NewSchema([]arrow.Field{
655+
{
656+
Name: "value",
657+
Type: arrow.PrimitiveTypes.Uint64,
658+
Nullable: true,
659+
Metadata: arrow.MetadataFrom(map[string]string{
660+
"sql.column_name": "value",
661+
"sql.database_type_name": "BIGINT UNSIGNED",
662+
}),
663+
},
664+
}, nil),
665+
expected: `[{"value": 10000000000000000000}]`,
666+
},
598667
} {
599668
s.Run(testCase.name, func() {
600669
s.NoError(s.stmt.SetSqlQuery(testCase.query))

0 commit comments

Comments
 (0)