From 405def284e2de92a8d59d12ae4f190ba1c8434bb Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Wed, 2 Sep 2026 21:46:48 +0900 Subject: [PATCH 1/8] Add TINYINT(1) bool conversion --- rows.go | 46 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/rows.go b/rows.go index 190e75f9b..f5067123e 100644 --- a/rows.go +++ b/rows.go @@ -9,6 +9,7 @@ package mysql import ( + "database/sql" "database/sql/driver" "io" "math" @@ -59,7 +60,20 @@ func (rows *mysqlRows) Columns() []string { return columns } +func (rows *mysqlRows) tinyInt1IsBool(i int) bool { + if rows.mc == nil || !rows.mc.cfg.tinyInt1IsBool { + return false + } + column := rows.rs.columns[i] + return column.fieldType == fieldTypeTiny && + column.length == 1 && + column.flags&(flagUnsigned|flagZeroFill) == 0 +} + func (rows *mysqlRows) ColumnTypeDatabaseTypeName(i int) string { + if rows.tinyInt1IsBool(i) { + return "BOOLEAN" + } return rows.rs.columns[i].typeDatabaseName() } @@ -94,9 +108,29 @@ func (rows *mysqlRows) ColumnTypePrecisionScale(i int) (int64, int64, bool) { } func (rows *mysqlRows) ColumnTypeScanType(i int) reflect.Type { + if rows.tinyInt1IsBool(i) { + if rows.rs.columns[i].flags&flagNotNULL != 0 { + return reflect.TypeFor[bool]() + } + return reflect.TypeFor[sql.NullBool]() + } return rows.rs.columns[i].scanType() } +func (rows *mysqlRows) convertTinyInt1ToBool(dest []driver.Value) { + if rows.mc == nil || !rows.mc.cfg.tinyInt1IsBool { + return + } + for i, v := range dest { + if !rows.tinyInt1IsBool(i) || v == nil { + continue + } + if n, ok := v.(int64); ok { + dest[i] = n != 0 + } + } +} + func (rows *mysqlRows) Close() (err error) { if f := rows.finish; f != nil { f() @@ -197,7 +231,11 @@ func (rows *binaryRows) Next(dest []driver.Value) error { } // Fetch next row from stream - return rows.readRow(dest) + if err := rows.readRow(dest); err != nil { + return err + } + rows.convertTinyInt1ToBool(dest) + return nil } return io.EOF } @@ -219,7 +257,11 @@ func (rows *textRows) Next(dest []driver.Value) error { } // Fetch next row from stream - return rows.readRow(dest) + if err := rows.readRow(dest); err != nil { + return err + } + rows.convertTinyInt1ToBool(dest) + return nil } return io.EOF } From 187219ba9cb421b7ef0c2abab1ca9d59a5268893 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Wed, 2 Sep 2026 21:48:03 +0900 Subject: [PATCH 2/8] Add tinyInt1IsBool option --- dsn.go | 37 +++++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/dsn.go b/dsn.go index 41463a503..09cd996c2 100644 --- a/dsn.go +++ b/dsn.go @@ -76,7 +76,8 @@ type Config struct { // unexported fields. new options should be come here. // boolean first. alphabetical order. - compress bool // Enable zlib compression + compress bool // Enable zlib compression + tinyInt1IsBool bool // Treat signed TINYINT(1) as boolean beforeConnect func(context.Context, *Config) error // Invoked before a connection is established pubKey *rsa.PublicKey // Server public key @@ -136,6 +137,14 @@ func EnableCompression(yes bool) Option { } } +// TinyInt1IsBool controls whether signed TINYINT(1) columns are treated as boolean. +func TinyInt1IsBool(yes bool) Option { + return func(cfg *Config) error { + cfg.tinyInt1IsBool = yes + return nil + } +} + // Charset sets the connection charset and collation. // // charset is the connection charset. @@ -355,6 +364,10 @@ func (cfg *Config) FormatDSN() string { writeDSNParam(&buf, &hasParam, "timeTruncate", cfg.timeTruncate.String()) } + if cfg.tinyInt1IsBool { + writeDSNParam(&buf, &hasParam, "tinyInt1IsBool", "true") + } + if cfg.ReadTimeout > 0 { writeDSNParam(&buf, &hasParam, "readTimeout", cfg.ReadTimeout.String()) } @@ -603,13 +616,6 @@ func parseDSNParams(cfg *Config, params string) (err error) { return errors.New("invalid bool value: " + value) } - // time.Time truncation - case "timeTruncate": - cfg.timeTruncate, err = time.ParseDuration(value) - if err != nil { - return fmt.Errorf("invalid timeTruncate value: %v, error: %w", value, err) - } - // I/O read Timeout case "readTimeout": cfg.ReadTimeout, err = time.ParseDuration(value) @@ -644,6 +650,21 @@ func parseDSNParams(cfg *Config, params string) (err error) { return } + // time.Time truncation + case "timeTruncate": + cfg.timeTruncate, err = time.ParseDuration(value) + if err != nil { + return fmt.Errorf("invalid timeTruncate value: %v, error: %w", value, err) + } + + // Treat TINYINT(1) as boolean + case "tinyInt1IsBool": + var isBool bool + cfg.tinyInt1IsBool, isBool = readBool(value) + if !isBool { + return errors.New("invalid bool value: " + value) + } + // TLS-Encryption case "tls": boolValue, isBool := readBool(value) From 77556daec868d00c952ca5032b396d910b2bfe61 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Wed, 2 Sep 2026 21:48:55 +0900 Subject: [PATCH 3/8] Test TINYINT(1) bool option --- tinyint1_test.go | 115 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 tinyint1_test.go diff --git a/tinyint1_test.go b/tinyint1_test.go new file mode 100644 index 000000000..f2c7eba9f --- /dev/null +++ b/tinyint1_test.go @@ -0,0 +1,115 @@ +// Go MySQL Driver - A MySQL-Driver for Go's database/sql package +// +// Copyright 2026 The Go-MySQL-Driver Authors. All rights reserved. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at http://mozilla.org/MPL/2.0/. + +package mysql + +import ( + "database/sql" + "reflect" + "strings" + "testing" +) + +func TestTinyInt1IsBoolConfig(t *testing.T) { + cfg := NewConfig() + if cfg.tinyInt1IsBool { + t.Fatal("tinyInt1IsBool should be disabled by default") + } + + if err := cfg.Apply(TinyInt1IsBool(true)); err != nil { + t.Fatal(err) + } + if !cfg.tinyInt1IsBool { + t.Fatal("TinyInt1IsBool(true) did not enable the option") + } + if got := cfg.FormatDSN(); !strings.Contains(got, "tinyInt1IsBool=true") { + t.Fatalf("FormatDSN() = %q; want tinyInt1IsBool=true", got) + } + + cfg, err := ParseDSN("/?tinyInt1IsBool=true") + if err != nil { + t.Fatal(err) + } + if !cfg.tinyInt1IsBool { + t.Fatal("ParseDSN did not enable tinyInt1IsBool") + } + + if _, err := ParseDSN("/?tinyInt1IsBool=invalid"); err == nil { + t.Fatal("ParseDSN accepted invalid tinyInt1IsBool value") + } +} + +func TestTinyInt1IsBool(t *testing.T) { + runTestsParallel(t, dsn+"&tinyInt1IsBool=true", func(dbt *DBTest, tbl string) { + dbt.mustExec("CREATE TABLE " + tbl + " (" + + "id INT PRIMARY KEY, " + + "b TINYINT(1) NOT NULL, " + + "bn TINYINT(1), " + + "n TINYINT(2) NOT NULL, " + + "u TINYINT(1) UNSIGNED NOT NULL)") + dbt.mustExec("INSERT INTO "+tbl+" VALUES " + + "(1, 0, NULL, 2, 1), " + + "(2, 1, 0, 2, 1), " + + "(3, 2, -1, 2, 1)") + + rows := dbt.mustQuery("SELECT b, bn, n, u FROM " + tbl + " ORDER BY id") + defer rows.Close() + + columnTypes, err := rows.ColumnTypes() + if err != nil { + dbt.Fatal(err) + } + wantDatabaseTypes := []string{"BOOLEAN", "BOOLEAN", "TINYINT", "UNSIGNED TINYINT"} + wantScanTypes := []reflect.Type{ + reflect.TypeFor[bool](), + reflect.TypeFor[sql.NullBool](), + scanTypeInt8, + scanTypeUint8, + } + for i, columnType := range columnTypes { + if got := columnType.DatabaseTypeName(); got != wantDatabaseTypes[i] { + dbt.Errorf("column %d DatabaseTypeName() = %q; want %q", i, got, wantDatabaseTypes[i]) + } + if got := columnType.ScanType(); got != wantScanTypes[i] { + dbt.Errorf("column %d ScanType() = %v; want %v", i, got, wantScanTypes[i]) + } + } + + want := [][4]any{ + {false, nil, int64(2), int64(1)}, + {true, false, int64(2), int64(1)}, + {true, true, int64(2), int64(1)}, + } + for row := 0; rows.Next(); row++ { + var got [4]any + if err := rows.Scan(&got[0], &got[1], &got[2], &got[3]); err != nil { + dbt.Fatal(err) + } + if !reflect.DeepEqual(got, want[row]) { + dbt.Errorf("row %d = %#v; want %#v", row, got, want[row]) + } + } + if err := rows.Err(); err != nil { + dbt.Fatal(err) + } + + stmt, err := dbt.db.Prepare("SELECT b, bn, n, u FROM " + tbl + " WHERE id = ?") + if err != nil { + dbt.Fatal(err) + } + defer stmt.Close() + + var got [4]any + if err := stmt.QueryRow(3).Scan(&got[0], &got[1], &got[2], &got[3]); err != nil { + dbt.Fatal(err) + } + if !reflect.DeepEqual(got, want[2]) { + dbt.Errorf("prepared statement row = %#v; want %#v", got, want[2]) + } + }) +} From 6ccee6e38ebeba61149484c2cf676953dae62446 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Wed, 2 Sep 2026 21:58:03 +0900 Subject: [PATCH 4/8] Enable TINYINT(1) bool handling by default --- dsn.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dsn.go b/dsn.go index 09cd996c2..74dc6d901 100644 --- a/dsn.go +++ b/dsn.go @@ -97,6 +97,7 @@ func NewConfig() *Config { Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true, + tinyInt1IsBool: true, } return cfg } @@ -364,8 +365,8 @@ func (cfg *Config) FormatDSN() string { writeDSNParam(&buf, &hasParam, "timeTruncate", cfg.timeTruncate.String()) } - if cfg.tinyInt1IsBool { - writeDSNParam(&buf, &hasParam, "tinyInt1IsBool", "true") + if !cfg.tinyInt1IsBool { + writeDSNParam(&buf, &hasParam, "tinyInt1IsBool", "false") } if cfg.ReadTimeout > 0 { From 0bbc51e06f9a7d8c3e3b9bc05a679bbecd216a71 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Wed, 2 Sep 2026 21:58:27 +0900 Subject: [PATCH 5/8] Update TINYINT(1) bool tests for default true --- tinyint1_test.go | 40 +++++++++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/tinyint1_test.go b/tinyint1_test.go index f2c7eba9f..f7b68f021 100644 --- a/tinyint1_test.go +++ b/tinyint1_test.go @@ -17,26 +17,29 @@ import ( func TestTinyInt1IsBoolConfig(t *testing.T) { cfg := NewConfig() - if cfg.tinyInt1IsBool { - t.Fatal("tinyInt1IsBool should be disabled by default") + if !cfg.tinyInt1IsBool { + t.Fatal("tinyInt1IsBool should be enabled by default") + } + if got := cfg.FormatDSN(); strings.Contains(got, "tinyInt1IsBool") { + t.Fatalf("FormatDSN() = %q; default option should be omitted", got) } - if err := cfg.Apply(TinyInt1IsBool(true)); err != nil { + if err := cfg.Apply(TinyInt1IsBool(false)); err != nil { t.Fatal(err) } - if !cfg.tinyInt1IsBool { - t.Fatal("TinyInt1IsBool(true) did not enable the option") + if cfg.tinyInt1IsBool { + t.Fatal("TinyInt1IsBool(false) did not disable the option") } - if got := cfg.FormatDSN(); !strings.Contains(got, "tinyInt1IsBool=true") { - t.Fatalf("FormatDSN() = %q; want tinyInt1IsBool=true", got) + if got := cfg.FormatDSN(); !strings.Contains(got, "tinyInt1IsBool=false") { + t.Fatalf("FormatDSN() = %q; want tinyInt1IsBool=false", got) } - cfg, err := ParseDSN("/?tinyInt1IsBool=true") + cfg, err := ParseDSN("/?tinyInt1IsBool=false") if err != nil { t.Fatal(err) } - if !cfg.tinyInt1IsBool { - t.Fatal("ParseDSN did not enable tinyInt1IsBool") + if cfg.tinyInt1IsBool { + t.Fatal("ParseDSN did not disable tinyInt1IsBool") } if _, err := ParseDSN("/?tinyInt1IsBool=invalid"); err == nil { @@ -45,7 +48,7 @@ func TestTinyInt1IsBoolConfig(t *testing.T) { } func TestTinyInt1IsBool(t *testing.T) { - runTestsParallel(t, dsn+"&tinyInt1IsBool=true", func(dbt *DBTest, tbl string) { + runTestsParallel(t, dsn, func(dbt *DBTest, tbl string) { dbt.mustExec("CREATE TABLE " + tbl + " (" + "id INT PRIMARY KEY, " + "b TINYINT(1) NOT NULL, " + @@ -113,3 +116,18 @@ func TestTinyInt1IsBool(t *testing.T) { } }) } + +func TestTinyInt1IsBoolDisabled(t *testing.T) { + runTestsParallel(t, dsn+"&tinyInt1IsBool=false", func(dbt *DBTest, tbl string) { + dbt.mustExec("CREATE TABLE " + tbl + " (b TINYINT(1) NOT NULL)") + dbt.mustExec("INSERT INTO " + tbl + " VALUES (2)") + + var got any + if err := dbt.db.QueryRow("SELECT b FROM " + tbl).Scan(&got); err != nil { + dbt.Fatal(err) + } + if got != int64(2) { + dbt.Fatalf("Scan(&any) = %#v; want int64(2)", got) + } + }) +} From 4ba783dfe3919c53b125fd9f1438d24707bdb3f2 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Wed, 2 Sep 2026 22:39:14 +0900 Subject: [PATCH 6/8] fix test --- driver_test.go | 11 +++++++---- dsn_test.go | 40 ++++++++++++++++++++-------------------- tinyint1_test.go | 2 +- 3 files changed, 28 insertions(+), 25 deletions(-) diff --git a/driver_test.go b/driver_test.go index 761236f54..03486859c 100644 --- a/driver_test.go +++ b/driver_test.go @@ -421,8 +421,8 @@ func TestNumbersToAny(t *testing.T) { if err != nil { dbt.Fatal(err) } - if b.(int64) != 1 { - dbt.Errorf("b != 1") + if b != true { + dbt.Errorf("b = %#v; want true", b) } if i8.(int64) != 127 { dbt.Errorf("i8 != 127") @@ -3053,6 +3053,9 @@ func TestRowsColumnTypes(t *testing.T) { ni0 := sql.NullInt64{Int64: 0, Valid: true} ni1 := sql.NullInt64{Int64: 1, Valid: true} ni42 := sql.NullInt64{Int64: 42, Valid: true} + nbNULL := sql.NullBool{Bool: false, Valid: false} + nb0 := sql.NullBool{Bool: false, Valid: true} + nb1 := sql.NullBool{Bool: true, Valid: true} nfNULL := sql.NullFloat64{Float64: 0.0, Valid: false} nf0 := sql.NullFloat64{Float64: 0.0, Valid: true} nf1337 := sql.NullFloat64{Float64: 13.37, Valid: true} @@ -3088,8 +3091,8 @@ func TestRowsColumnTypes(t *testing.T) { valuesOut [3]any }{ {"bit8null", "BIT(8)", "BIT", scanTypeBytes, true, 0, 0, [3]string{"0x0", "NULL", "0x42"}, [3]any{bx0, bNULL, bx42}}, - {"boolnull", "BOOL", "TINYINT", scanTypeNullInt, true, 0, 0, [3]string{"NULL", "true", "0"}, [3]any{niNULL, ni1, ni0}}, - {"bool", "BOOL NOT NULL", "TINYINT", scanTypeInt8, false, 0, 0, [3]string{"1", "0", "FALSE"}, [3]any{int8(1), int8(0), int8(0)}}, + {"boolnull", "BOOL", "BOOLEAN", reflect.TypeFor[sql.NullBool](), true, 0, 0, [3]string{"NULL", "true", "0"}, [3]any{nbNULL, nb1, nb0}}, + {"bool", "BOOL NOT NULL", "BOOLEAN", reflect.TypeFor[bool](), false, 0, 0, [3]string{"1", "0", "FALSE"}, [3]any{true, false, false}}, {"intnull", "INTEGER", "INT", scanTypeNullInt, true, 0, 0, [3]string{"0", "NULL", "42"}, [3]any{ni0, niNULL, ni42}}, {"smallint", "SMALLINT NOT NULL", "SMALLINT", scanTypeInt16, false, 0, 0, [3]string{"0", "-32768", "32767"}, [3]any{int16(0), int16(-32768), int16(32767)}}, {"smallintnull", "SMALLINT", "SMALLINT", scanTypeNullInt, true, 0, 0, [3]string{"0", "NULL", "42"}, [3]any{ni0, niNULL, ni42}}, diff --git a/dsn_test.go b/dsn_test.go index 0c8ac7a04..131f8a981 100644 --- a/dsn_test.go +++ b/dsn_test.go @@ -22,64 +22,64 @@ var testDSNs = []struct { out *Config }{{ "username:password@protocol(address)/dbname?param=value", - &Config{User: "username", Passwd: "password", Net: "protocol", Addr: "address", DBName: "dbname", Params: map[string]string{"param": "value"}, Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true}, + &Config{User: "username", Passwd: "password", Net: "protocol", Addr: "address", DBName: "dbname", Params: map[string]string{"param": "value"}, Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true, tinyInt1IsBool: true}, }, { "username:password@protocol(address)/dbname?param=value&columnsWithAlias=true", - &Config{User: "username", Passwd: "password", Net: "protocol", Addr: "address", DBName: "dbname", Params: map[string]string{"param": "value"}, Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true, ColumnsWithAlias: true}, + &Config{User: "username", Passwd: "password", Net: "protocol", Addr: "address", DBName: "dbname", Params: map[string]string{"param": "value"}, Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true, tinyInt1IsBool: true, ColumnsWithAlias: true}, }, { "username:password@protocol(address)/dbname?param=value&columnsWithAlias=true&multiStatements=true", - &Config{User: "username", Passwd: "password", Net: "protocol", Addr: "address", DBName: "dbname", Params: map[string]string{"param": "value"}, Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true, ColumnsWithAlias: true, MultiStatements: true}, + &Config{User: "username", Passwd: "password", Net: "protocol", Addr: "address", DBName: "dbname", Params: map[string]string{"param": "value"}, Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true, tinyInt1IsBool: true, ColumnsWithAlias: true, MultiStatements: true}, }, { "user@unix(/path/to/socket)/dbname?charset=utf8", - &Config{User: "user", Net: "unix", Addr: "/path/to/socket", DBName: "dbname", charsets: []string{"utf8"}, Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true}, + &Config{User: "user", Net: "unix", Addr: "/path/to/socket", DBName: "dbname", charsets: []string{"utf8"}, Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true, tinyInt1IsBool: true}, }, { "user:password@tcp(localhost:5555)/dbname?charset=utf8&tls=true", - &Config{User: "user", Passwd: "password", Net: "tcp", Addr: "localhost:5555", DBName: "dbname", charsets: []string{"utf8"}, Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true, TLSConfig: "true"}, + &Config{User: "user", Passwd: "password", Net: "tcp", Addr: "localhost:5555", DBName: "dbname", charsets: []string{"utf8"}, Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true, tinyInt1IsBool: true, TLSConfig: "true"}, }, { "user:password@tcp(localhost:5555)/dbname?charset=utf8mb4,utf8&tls=skip-verify", - &Config{User: "user", Passwd: "password", Net: "tcp", Addr: "localhost:5555", DBName: "dbname", charsets: []string{"utf8mb4", "utf8"}, Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true, TLSConfig: "skip-verify"}, + &Config{User: "user", Passwd: "password", Net: "tcp", Addr: "localhost:5555", DBName: "dbname", charsets: []string{"utf8mb4", "utf8"}, Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true, tinyInt1IsBool: true, TLSConfig: "skip-verify"}, }, { "user:password@/dbname?loc=UTC&timeout=30s&readTimeout=1s&writeTimeout=1s&allowAllFiles=1&clientFoundRows=true&allowOldPasswords=TRUE&collation=utf8mb4_unicode_ci&maxAllowedPacket=16777216&tls=false&allowCleartextPasswords=true&parseTime=true&rejectReadOnly=true", - &Config{User: "user", Passwd: "password", Net: "tcp", Addr: "127.0.0.1:3306", DBName: "dbname", Collation: "utf8mb4_unicode_ci", Loc: time.UTC, TLSConfig: "false", AllowCleartextPasswords: true, AllowNativePasswords: true, Timeout: 30 * time.Second, ReadTimeout: time.Second, WriteTimeout: time.Second, Logger: defaultLogger, AllowAllFiles: true, AllowOldPasswords: true, CheckConnLiveness: true, ClientFoundRows: true, MaxAllowedPacket: 16777216, ParseTime: true, RejectReadOnly: true}, + &Config{User: "user", Passwd: "password", Net: "tcp", Addr: "127.0.0.1:3306", DBName: "dbname", Collation: "utf8mb4_unicode_ci", Loc: time.UTC, TLSConfig: "false", AllowCleartextPasswords: true, AllowNativePasswords: true, Timeout: 30 * time.Second, ReadTimeout: time.Second, WriteTimeout: time.Second, Logger: defaultLogger, AllowAllFiles: true, AllowOldPasswords: true, CheckConnLiveness: true, tinyInt1IsBool: true, ClientFoundRows: true, MaxAllowedPacket: 16777216, ParseTime: true, RejectReadOnly: true}, }, { "user:password@/dbname?allowNativePasswords=false&checkConnLiveness=false&maxAllowedPacket=0&allowFallbackToPlaintext=true", - &Config{User: "user", Passwd: "password", Net: "tcp", Addr: "127.0.0.1:3306", DBName: "dbname", Loc: time.UTC, MaxAllowedPacket: 0, Logger: defaultLogger, AllowFallbackToPlaintext: true, AllowNativePasswords: false, CheckConnLiveness: false}, + &Config{User: "user", Passwd: "password", Net: "tcp", Addr: "127.0.0.1:3306", DBName: "dbname", Loc: time.UTC, MaxAllowedPacket: 0, Logger: defaultLogger, AllowFallbackToPlaintext: true, AllowNativePasswords: false, CheckConnLiveness: false, tinyInt1IsBool: true}, }, { "user:p@ss(word)@tcp([de:ad:be:ef::ca:fe]:80)/dbname?loc=Local", - &Config{User: "user", Passwd: "p@ss(word)", Net: "tcp", Addr: "[de:ad:be:ef::ca:fe]:80", DBName: "dbname", Loc: time.Local, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true}, + &Config{User: "user", Passwd: "p@ss(word)", Net: "tcp", Addr: "[de:ad:be:ef::ca:fe]:80", DBName: "dbname", Loc: time.Local, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true, tinyInt1IsBool: true}, }, { "/dbname", - &Config{Net: "tcp", Addr: "127.0.0.1:3306", DBName: "dbname", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true}, + &Config{Net: "tcp", Addr: "127.0.0.1:3306", DBName: "dbname", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true, tinyInt1IsBool: true}, }, { "/dbname%2Fwithslash", - &Config{Net: "tcp", Addr: "127.0.0.1:3306", DBName: "dbname/withslash", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true}, + &Config{Net: "tcp", Addr: "127.0.0.1:3306", DBName: "dbname/withslash", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true, tinyInt1IsBool: true}, }, { "@/", - &Config{Net: "tcp", Addr: "127.0.0.1:3306", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true}, + &Config{Net: "tcp", Addr: "127.0.0.1:3306", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true, tinyInt1IsBool: true}, }, { "/", - &Config{Net: "tcp", Addr: "127.0.0.1:3306", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true}, + &Config{Net: "tcp", Addr: "127.0.0.1:3306", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true, tinyInt1IsBool: true}, }, { "", - &Config{Net: "tcp", Addr: "127.0.0.1:3306", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true}, + &Config{Net: "tcp", Addr: "127.0.0.1:3306", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true, tinyInt1IsBool: true}, }, { "user:p@/ssword@/", - &Config{User: "user", Passwd: "p@/ssword", Net: "tcp", Addr: "127.0.0.1:3306", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true}, + &Config{User: "user", Passwd: "p@/ssword", Net: "tcp", Addr: "127.0.0.1:3306", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true, tinyInt1IsBool: true}, }, { "unix/?arg=%2Fsome%2Fpath.ext", - &Config{Net: "unix", Addr: "/tmp/mysql.sock", Params: map[string]string{"arg": "/some/path.ext"}, Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true}, + &Config{Net: "unix", Addr: "/tmp/mysql.sock", Params: map[string]string{"arg": "/some/path.ext"}, Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true, tinyInt1IsBool: true}, }, { "tcp(127.0.0.1)/dbname", - &Config{Net: "tcp", Addr: "127.0.0.1:3306", DBName: "dbname", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true}, + &Config{Net: "tcp", Addr: "127.0.0.1:3306", DBName: "dbname", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true, tinyInt1IsBool: true}, }, { "tcp(de:ad:be:ef::ca:fe)/dbname", - &Config{Net: "tcp", Addr: "[de:ad:be:ef::ca:fe]:3306", DBName: "dbname", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true}, + &Config{Net: "tcp", Addr: "[de:ad:be:ef::ca:fe]:3306", DBName: "dbname", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true, tinyInt1IsBool: true}, }, { "user:password@/dbname?loc=UTC&timeout=30s&parseTime=true&timeTruncate=1h", - &Config{User: "user", Passwd: "password", Net: "tcp", Addr: "127.0.0.1:3306", DBName: "dbname", Loc: time.UTC, Timeout: 30 * time.Second, ParseTime: true, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true, timeTruncate: time.Hour}, + &Config{User: "user", Passwd: "password", Net: "tcp", Addr: "127.0.0.1:3306", DBName: "dbname", Loc: time.UTC, Timeout: 30 * time.Second, ParseTime: true, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true, tinyInt1IsBool: true, timeTruncate: time.Hour}, }, { "foo:bar@tcp(192.168.1.50:3307)/baz?timeout=10s&connectionAttributes=program_name:MySQLGoDriver%2FTest,program_version:1.2.3", - &Config{User: "foo", Passwd: "bar", Net: "tcp", Addr: "192.168.1.50:3307", DBName: "baz", Loc: time.UTC, Timeout: 10 * time.Second, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true, ConnectionAttributes: "program_name:MySQLGoDriver/Test,program_version:1.2.3"}, + &Config{User: "foo", Passwd: "bar", Net: "tcp", Addr: "192.168.1.50:3307", DBName: "baz", Loc: time.UTC, Timeout: 10 * time.Second, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true, tinyInt1IsBool: true, ConnectionAttributes: "program_name:MySQLGoDriver/Test,program_version:1.2.3"}, }, } diff --git a/tinyint1_test.go b/tinyint1_test.go index f7b68f021..66e977ff7 100644 --- a/tinyint1_test.go +++ b/tinyint1_test.go @@ -55,7 +55,7 @@ func TestTinyInt1IsBool(t *testing.T) { "bn TINYINT(1), " + "n TINYINT(2) NOT NULL, " + "u TINYINT(1) UNSIGNED NOT NULL)") - dbt.mustExec("INSERT INTO "+tbl+" VALUES " + + dbt.mustExec("INSERT INTO " + tbl + " VALUES " + "(1, 0, NULL, 2, 1), " + "(2, 1, 0, 2, 1), " + "(3, 2, -1, 2, 1)") From 077fdea39edf26b9cbb6326c2c31ebc5573769fb Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Wed, 2 Sep 2026 23:57:09 +0900 Subject: [PATCH 7/8] rename tinyint1_test -> boolean_test --- tinyint1_test.go => boolean_test.go | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tinyint1_test.go => boolean_test.go (100%) diff --git a/tinyint1_test.go b/boolean_test.go similarity index 100% rename from tinyint1_test.go rename to boolean_test.go From 86bb61dd86c7fb5fe0e53844bb010fad16ae5cbf Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Thu, 3 Sep 2026 01:04:19 +0900 Subject: [PATCH 8/8] fixup --- README.md | 12 ++++++++++++ boolean_test.go | 36 +++++++++++++++++++++++++++--------- 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index ccbe6d078..45376e032 100644 --- a/README.md +++ b/README.md @@ -319,6 +319,18 @@ Default: 0 > [!NOTE] > `time.Time` arguments are sent with up to nanosecond precision, so a value from `time.Now()` usually has more fractional-second digits than a `DATETIME(N)` or `TIMESTAMP(N)` column stores. On MariaDB, comparing such a value against an indexed column can prevent an index range scan, turning it into a full index scan. Truncating to the column's precision (`1us` for `DATETIME(6)`) avoids this. Only arguments sent to the server are truncated; values read from the server are not affected. +##### `tinyInt1IsBool` + +``` +Type: bool +Valid Values: true, false +Default: true +``` + +When `tinyInt1IsBool=true`, signed `TINYINT(1)` columns are treated as boolean values. Zero is returned as `false`, and non-zero values are returned as `true`. Their database type name is reported as `BOOLEAN`, and their scan type is `bool` for non-nullable columns or `sql.NullBool` for nullable columns. + +Unsigned and `ZEROFILL` columns are not converted. Set `tinyInt1IsBool=false` to preserve the numeric `TINYINT` behavior. + ##### `maxAllowedPacket` ``` Type: decimal number diff --git a/boolean_test.go b/boolean_test.go index 66e977ff7..4ccd06c45 100644 --- a/boolean_test.go +++ b/boolean_test.go @@ -58,7 +58,8 @@ func TestTinyInt1IsBool(t *testing.T) { dbt.mustExec("INSERT INTO " + tbl + " VALUES " + "(1, 0, NULL, 2, 1), " + "(2, 1, 0, 2, 1), " + - "(3, 2, -1, 2, 1)") + "(3, 2, -1, 2, 1), " + + "(4, 0, 0, 2, 1)") rows := dbt.mustQuery("SELECT b, bn, n, u FROM " + tbl + " ORDER BY id") defer rows.Close() @@ -87,12 +88,18 @@ func TestTinyInt1IsBool(t *testing.T) { {false, nil, int64(2), int64(1)}, {true, false, int64(2), int64(1)}, {true, true, int64(2), int64(1)}, + {false, false, int64(2), int64(1)}, } - for row := 0; rows.Next(); row++ { + row := 0 + for ; rows.Next(); row++ { var got [4]any if err := rows.Scan(&got[0], &got[1], &got[2], &got[3]); err != nil { dbt.Fatal(err) } + if row >= len(want) { + dbt.Errorf("unexpected row %d = %#v", row, got) + continue + } if !reflect.DeepEqual(got, want[row]) { dbt.Errorf("row %d = %#v; want %#v", row, got, want[row]) } @@ -100,6 +107,9 @@ func TestTinyInt1IsBool(t *testing.T) { if err := rows.Err(); err != nil { dbt.Fatal(err) } + if row != len(want) { + dbt.Errorf("got %d rows; want %d", row, len(want)) + } stmt, err := dbt.db.Prepare("SELECT b, bn, n, u FROM " + tbl + " WHERE id = ?") if err != nil { @@ -107,12 +117,14 @@ func TestTinyInt1IsBool(t *testing.T) { } defer stmt.Close() - var got [4]any - if err := stmt.QueryRow(3).Scan(&got[0], &got[1], &got[2], &got[3]); err != nil { - dbt.Fatal(err) - } - if !reflect.DeepEqual(got, want[2]) { - dbt.Errorf("prepared statement row = %#v; want %#v", got, want[2]) + for _, id := range []int{3, 4} { + var got [4]any + if err := stmt.QueryRow(id).Scan(&got[0], &got[1], &got[2], &got[3]); err != nil { + dbt.Fatal(err) + } + if !reflect.DeepEqual(got, want[id-1]) { + dbt.Errorf("prepared statement row %d = %#v; want %#v", id, got, want[id-1]) + } } }) } @@ -122,8 +134,14 @@ func TestTinyInt1IsBoolDisabled(t *testing.T) { dbt.mustExec("CREATE TABLE " + tbl + " (b TINYINT(1) NOT NULL)") dbt.mustExec("INSERT INTO " + tbl + " VALUES (2)") + stmt, err := dbt.db.Prepare("SELECT b FROM " + tbl + " WHERE b = ?") + if err != nil { + dbt.Fatal(err) + } + defer stmt.Close() + var got any - if err := dbt.db.QueryRow("SELECT b FROM " + tbl).Scan(&got); err != nil { + if err := stmt.QueryRow(2).Scan(&got); err != nil { dbt.Fatal(err) } if got != int64(2) {