From 9d6b46c54a62acd6c04342f6cffb0ce03bf0f791 Mon Sep 17 00:00:00 2001 From: Ryo Kitagawa Date: Thu, 29 Jan 2026 13:12:36 +0900 Subject: [PATCH 1/3] fix(mysql): Convert boolean literals (true/false) to bool type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, MySQL boolean literals like `SELECT true` were incorrectly converted to int32 type, while table columns with BOOL/BOOLEAN/TINYINT(1) types were correctly converted to bool. The TiDB parser represents boolean literals as mysql.TypeLonglong with value 1 (true) or 0 (false), which was indistinguishable from integer literals like `SELECT 1`. This fix: - Detects boolean literals by examining the original SQL text - Converts `true`/`false` (case-insensitive) to bool type - Preserves integer literals like `SELECT 1` as int32 - Supports all case variations: TRUE, True, FALSE, False, etc. Test cases: - SELECT true/false → bool - SELECT TRUE/FALSE → bool - SELECT true AS col → bool - SELECT 1/0 → int32 (unchanged) --- .../select_true_literal/mysql/db/db.go | 31 ++++++++++ .../select_true_literal/mysql/db/models.go | 5 ++ .../select_true_literal/mysql/db/query.sql.go | 60 +++++++++++++++++++ .../select_true_literal/mysql/query.sql | 11 ++++ .../select_true_literal/mysql/sqlc.json | 10 ++++ internal/engine/dolphin/convert.go | 45 ++++++++++++++ internal/engine/dolphin/parse.go | 2 +- 7 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 internal/endtoend/testdata/select_true_literal/mysql/db/db.go create mode 100644 internal/endtoend/testdata/select_true_literal/mysql/db/models.go create mode 100644 internal/endtoend/testdata/select_true_literal/mysql/db/query.sql.go create mode 100644 internal/endtoend/testdata/select_true_literal/mysql/query.sql create mode 100644 internal/endtoend/testdata/select_true_literal/mysql/sqlc.json diff --git a/internal/endtoend/testdata/select_true_literal/mysql/db/db.go b/internal/endtoend/testdata/select_true_literal/mysql/db/db.go new file mode 100644 index 0000000000..cd5bbb8e08 --- /dev/null +++ b/internal/endtoend/testdata/select_true_literal/mysql/db/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/select_true_literal/mysql/db/models.go b/internal/endtoend/testdata/select_true_literal/mysql/db/models.go new file mode 100644 index 0000000000..32099017df --- /dev/null +++ b/internal/endtoend/testdata/select_true_literal/mysql/db/models.go @@ -0,0 +1,5 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db diff --git a/internal/endtoend/testdata/select_true_literal/mysql/db/query.sql.go b/internal/endtoend/testdata/select_true_literal/mysql/db/query.sql.go new file mode 100644 index 0000000000..f90928b2a0 --- /dev/null +++ b/internal/endtoend/testdata/select_true_literal/mysql/db/query.sql.go @@ -0,0 +1,60 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query.sql + +package db + +import ( + "context" +) + +const selectFalse = `-- name: SelectFalse :one +SELECT false +` + +func (q *Queries) SelectFalse(ctx context.Context) (bool, error) { + row := q.db.QueryRowContext(ctx, selectFalse) + var column_1 bool + err := row.Scan(&column_1) + return column_1, err +} + +const selectMultipleBooleans = `-- name: SelectMultipleBooleans :one +SELECT true AS col_a, false AS col_b, true AS col_c +` + +type SelectMultipleBooleansRow struct { + ColA bool + ColB bool + ColC bool +} + +func (q *Queries) SelectMultipleBooleans(ctx context.Context) (SelectMultipleBooleansRow, error) { + row := q.db.QueryRowContext(ctx, selectMultipleBooleans) + var i SelectMultipleBooleansRow + err := row.Scan(&i.ColA, &i.ColB, &i.ColC) + return i, err +} + +const selectTrue = `-- name: SelectTrue :one +SELECT true +` + +func (q *Queries) SelectTrue(ctx context.Context) (bool, error) { + row := q.db.QueryRowContext(ctx, selectTrue) + var column_1 bool + err := row.Scan(&column_1) + return column_1, err +} + +const selectTrueWithAlias = `-- name: SelectTrueWithAlias :one +SELECT true AS is_active +` + +func (q *Queries) SelectTrueWithAlias(ctx context.Context) (bool, error) { + row := q.db.QueryRowContext(ctx, selectTrueWithAlias) + var is_active bool + err := row.Scan(&is_active) + return is_active, err +} diff --git a/internal/endtoend/testdata/select_true_literal/mysql/query.sql b/internal/endtoend/testdata/select_true_literal/mysql/query.sql new file mode 100644 index 0000000000..5c95a13d5d --- /dev/null +++ b/internal/endtoend/testdata/select_true_literal/mysql/query.sql @@ -0,0 +1,11 @@ +-- name: SelectTrue :one +SELECT true; + +-- name: SelectFalse :one +SELECT false; + +-- name: SelectTrueWithAlias :one +SELECT true AS is_active; + +-- name: SelectMultipleBooleans :one +SELECT true AS col_a, false AS col_b, true AS col_c; diff --git a/internal/endtoend/testdata/select_true_literal/mysql/sqlc.json b/internal/endtoend/testdata/select_true_literal/mysql/sqlc.json new file mode 100644 index 0000000000..bb491bcc61 --- /dev/null +++ b/internal/endtoend/testdata/select_true_literal/mysql/sqlc.json @@ -0,0 +1,10 @@ +{ + "version": "1", + "packages": [ + { + "path": "db", + "engine": "mysql", + "queries": "query.sql" + } + ] +} diff --git a/internal/engine/dolphin/convert.go b/internal/engine/dolphin/convert.go index 1f68358ce4..03424e45e0 100644 --- a/internal/engine/dolphin/convert.go +++ b/internal/engine/dolphin/convert.go @@ -17,6 +17,7 @@ import ( type cc struct { paramCount int + sql string // Original SQL text for this statement } func todo(n pcast.Node) *ast.TODO { @@ -672,6 +673,41 @@ func (c *cc) convertUpdateStmt(n *pcast.UpdateStmt) *ast.UpdateStmt { return stmt } +// isBooleanLiteral checks if the ValueExpr represents a boolean literal (true/false) +// by examining the original SQL text +func (c *cc) isBooleanLiteral(n *driver.ValueExpr) bool { + if c.sql == "" { + return false + } + + pos := n.OriginTextPosition() + if pos < 0 || pos >= len(c.sql) { + return false + } + + // Extract the token from the SQL text + remaining := strings.ToLower(c.sql[pos:]) + + // Check if it starts with "true" or "false" and is followed by a non-identifier character + if strings.HasPrefix(remaining, "true") { + if len(remaining) == 4 || !isIdentifierChar(remaining[4]) { + return true + } + } + if strings.HasPrefix(remaining, "false") { + if len(remaining) == 5 || !isIdentifierChar(remaining[5]) { + return true + } + } + + return false +} + +// isIdentifierChar returns true if the character can be part of an identifier +func isIdentifierChar(c byte) bool { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' +} + func (c *cc) convertValueExpr(n *driver.ValueExpr) *ast.A_Const { switch n.TexprNode.Type.GetType() { case mysql.TypeBit: @@ -691,6 +727,15 @@ func (c *cc) convertValueExpr(n *driver.ValueExpr) *ast.A_Const { mysql.TypeYear, mysql.TypeLong, mysql.TypeLonglong: + // Check if this is a boolean literal (true/false) + if c.isBooleanLiteral(n) { + return &ast.A_Const{ + Val: &ast.Boolean{ + Boolval: n.Datum.GetInt64() != 0, + }, + Location: n.OriginTextPosition(), + } + } return &ast.A_Const{ Val: &ast.Integer{ Ival: n.Datum.GetInt64(), diff --git a/internal/engine/dolphin/parse.go b/internal/engine/dolphin/parse.go index 537f7ad64f..f4f8a81d63 100644 --- a/internal/engine/dolphin/parse.go +++ b/internal/engine/dolphin/parse.go @@ -59,7 +59,7 @@ func (p *Parser) Parse(r io.Reader) ([]ast.Statement, error) { } var stmts []ast.Statement for i := range stmtNodes { - converter := &cc{} + converter := &cc{sql: string(blob)} out := converter.convert(stmtNodes[i]) if _, ok := out.(*ast.TODO); ok { continue From ae1916858b900801747baa4a8ea6c7b553eb1f55 Mon Sep 17 00:00:00 2001 From: Ryo Kitagawa Date: Thu, 29 Jan 2026 13:20:37 +0900 Subject: [PATCH 2/3] fix(sqlite): Convert boolean literals (true/false) to bool type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, SQLite boolean literals like `SELECT true` were incorrectly converted to int64 type, while PostgreSQL correctly returned bool. The ANTLR parser provides TRUE_() and FALSE_() tokens, but these were originally implemented to return Integer in PR #1447 (2022-02-19) without specific reasoning, likely because SQLite internally uses integers (1/0) for boolean values. This fix changes the TRUE_()/FALSE_() handling to return ast.Boolean instead of ast.Integer, aligning SQLite's behavior with PostgreSQL and providing a more type-safe interface. Test results: - SELECT true/false → bool (was int64) - SELECT TRUE/FALSE → bool (was int64) - SELECT 1 → int64 (unchanged) Added test: internal/endtoend/testdata/select_true_literal/sqlite/ --- .../select_true_literal/sqlite/db/db.go | 31 ++++++++ .../select_true_literal/sqlite/db/models.go | 5 ++ .../sqlite/db/query.sql.go | 71 +++++++++++++++++++ .../select_true_literal/sqlite/query.sql | 14 ++++ .../select_true_literal/sqlite/sqlc.json | 10 +++ internal/engine/sqlite/convert.go | 7 +- 6 files changed, 132 insertions(+), 6 deletions(-) create mode 100644 internal/endtoend/testdata/select_true_literal/sqlite/db/db.go create mode 100644 internal/endtoend/testdata/select_true_literal/sqlite/db/models.go create mode 100644 internal/endtoend/testdata/select_true_literal/sqlite/db/query.sql.go create mode 100644 internal/endtoend/testdata/select_true_literal/sqlite/query.sql create mode 100644 internal/endtoend/testdata/select_true_literal/sqlite/sqlc.json diff --git a/internal/endtoend/testdata/select_true_literal/sqlite/db/db.go b/internal/endtoend/testdata/select_true_literal/sqlite/db/db.go new file mode 100644 index 0000000000..cd5bbb8e08 --- /dev/null +++ b/internal/endtoend/testdata/select_true_literal/sqlite/db/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/select_true_literal/sqlite/db/models.go b/internal/endtoend/testdata/select_true_literal/sqlite/db/models.go new file mode 100644 index 0000000000..32099017df --- /dev/null +++ b/internal/endtoend/testdata/select_true_literal/sqlite/db/models.go @@ -0,0 +1,5 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db diff --git a/internal/endtoend/testdata/select_true_literal/sqlite/db/query.sql.go b/internal/endtoend/testdata/select_true_literal/sqlite/db/query.sql.go new file mode 100644 index 0000000000..e2c27b5403 --- /dev/null +++ b/internal/endtoend/testdata/select_true_literal/sqlite/db/query.sql.go @@ -0,0 +1,71 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query.sql + +package db + +import ( + "context" +) + +const selectFalse = `-- name: SelectFalse :one +SELECT false +` + +func (q *Queries) SelectFalse(ctx context.Context) (bool, error) { + row := q.db.QueryRowContext(ctx, selectFalse) + var column_1 bool + err := row.Scan(&column_1) + return column_1, err +} + +const selectMultipleBooleans = `-- name: SelectMultipleBooleans :one +SELECT true AS col_a, false AS col_b, true AS col_c +` + +type SelectMultipleBooleansRow struct { + ColA bool + ColB bool + ColC bool +} + +func (q *Queries) SelectMultipleBooleans(ctx context.Context) (SelectMultipleBooleansRow, error) { + row := q.db.QueryRowContext(ctx, selectMultipleBooleans) + var i SelectMultipleBooleansRow + err := row.Scan(&i.ColA, &i.ColB, &i.ColC) + return i, err +} + +const selectOne = `-- name: SelectOne :one +SELECT 1 +` + +func (q *Queries) SelectOne(ctx context.Context) (int64, error) { + row := q.db.QueryRowContext(ctx, selectOne) + var column_1 int64 + err := row.Scan(&column_1) + return column_1, err +} + +const selectTrue = `-- name: SelectTrue :one +SELECT true +` + +func (q *Queries) SelectTrue(ctx context.Context) (bool, error) { + row := q.db.QueryRowContext(ctx, selectTrue) + var column_1 bool + err := row.Scan(&column_1) + return column_1, err +} + +const selectTrueWithAlias = `-- name: SelectTrueWithAlias :one +SELECT true AS is_active +` + +func (q *Queries) SelectTrueWithAlias(ctx context.Context) (bool, error) { + row := q.db.QueryRowContext(ctx, selectTrueWithAlias) + var is_active bool + err := row.Scan(&is_active) + return is_active, err +} diff --git a/internal/endtoend/testdata/select_true_literal/sqlite/query.sql b/internal/endtoend/testdata/select_true_literal/sqlite/query.sql new file mode 100644 index 0000000000..b667538f7c --- /dev/null +++ b/internal/endtoend/testdata/select_true_literal/sqlite/query.sql @@ -0,0 +1,14 @@ +-- name: SelectTrue :one +SELECT true; + +-- name: SelectFalse :one +SELECT false; + +-- name: SelectTrueWithAlias :one +SELECT true AS is_active; + +-- name: SelectMultipleBooleans :one +SELECT true AS col_a, false AS col_b, true AS col_c; + +-- name: SelectOne :one +SELECT 1; diff --git a/internal/endtoend/testdata/select_true_literal/sqlite/sqlc.json b/internal/endtoend/testdata/select_true_literal/sqlite/sqlc.json new file mode 100644 index 0000000000..b00ce26c02 --- /dev/null +++ b/internal/endtoend/testdata/select_true_literal/sqlite/sqlc.json @@ -0,0 +1,10 @@ +{ + "version": "1", + "packages": [ + { + "path": "db", + "engine": "sqlite", + "queries": "query.sql" + } + ] +} diff --git a/internal/engine/sqlite/convert.go b/internal/engine/sqlite/convert.go index e9868f5be6..a95bec9e4c 100644 --- a/internal/engine/sqlite/convert.go +++ b/internal/engine/sqlite/convert.go @@ -752,13 +752,8 @@ func (c *cc) convertLiteral(n *parser.Expr_literalContext) ast.Node { } if literal.TRUE_() != nil || literal.FALSE_() != nil { - var i int64 - if literal.TRUE_() != nil { - i = 1 - } - return &ast.A_Const{ - Val: &ast.Integer{Ival: i}, + Val: &ast.Boolean{Boolval: literal.TRUE_() != nil}, Location: n.GetStart().GetStart(), } } From 913160206d6d4dc715185c0667dc61a7997434a1 Mon Sep 17 00:00:00 2001 From: Ryo Kitagawa Date: Thu, 29 Jan 2026 16:41:19 +0900 Subject: [PATCH 3/3] fix: correct boolean literal type in generated code --- internal/endtoend/testdata/selectstatic/mysql/go/query.sql.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/endtoend/testdata/selectstatic/mysql/go/query.sql.go b/internal/endtoend/testdata/selectstatic/mysql/go/query.sql.go index c6cfed9b63..f269f37f22 100644 --- a/internal/endtoend/testdata/selectstatic/mysql/go/query.sql.go +++ b/internal/endtoend/testdata/selectstatic/mysql/go/query.sql.go @@ -17,7 +17,7 @@ type SelectStaticRow struct { Column1 string B string Num int32 - Truefield int32 + Truefield bool Floater float64 }