From e683da2db6194ba93f5721e1d5556f6c6e5f5216 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Wed, 25 Mar 2026 16:42:23 +0300 Subject: [PATCH] db.mysql: fix real_query to return proper mysql error (fixes #18059) --- vlib/db/mysql/mysql.c.v | 11 +++-- vlib/db/mysql/prepared_stmt_test.v | 67 ++++++++++++++++++++++++++++++ vlib/db/mysql/stmt.c.v | 18 ++++++-- vlib/db/mysql/utils.c.v | 10 +++++ 4 files changed, 100 insertions(+), 6 deletions(-) diff --git a/vlib/db/mysql/mysql.c.v b/vlib/db/mysql/mysql.c.v index f0cb5d18f..b3dcfc730 100644 --- a/vlib/db/mysql/mysql.c.v +++ b/vlib/db/mysql/mysql.c.v @@ -513,7 +513,7 @@ pub fn (db &DB) prepare(query string) !StmtHandle { mut code := C.mysql_stmt_prepare(stmt, query.str, query.len) if code != 0 { - db.throw_mysql_error()! + throw_mysql_stmt_error(stmt)! } return StmtHandle{ @@ -543,12 +543,12 @@ pub fn (stmt &StmtHandle) execute(params []string) ![]Row { mut response := C.mysql_stmt_bind_param(stmt.stmt, unsafe { &C.MYSQL_BIND(bind_params.data) }) if response == true { - stmt.db.throw_mysql_error()! + throw_mysql_stmt_error(stmt.stmt)! } mut code := C.mysql_stmt_execute(stmt.stmt) if code != 0 { - stmt.db.throw_mysql_error()! + throw_mysql_stmt_error(stmt.stmt)! } query_metadata := C.mysql_stmt_result_metadata(stmt.stmt) @@ -610,6 +610,11 @@ fn (db &DB) throw_mysql_error() ! { return error_with_code(get_error_msg(db.conn), get_errno(db.conn)) } +@[inline] +fn throw_mysql_stmt_error(stmt &C.MYSQL_STMT) ! { + return error_with_code(get_stmt_error_msg(stmt), get_stmt_errno(stmt)) +} + @[inline] fn (db &DB) check_connection_is_established() ! { if isnil(db.conn) { diff --git a/vlib/db/mysql/prepared_stmt_test.v b/vlib/db/mysql/prepared_stmt_test.v index 67949e68d..4ce0f06b8 100644 --- a/vlib/db/mysql/prepared_stmt_test.v +++ b/vlib/db/mysql/prepared_stmt_test.v @@ -100,3 +100,70 @@ fn test_stmt_bind_result_buffer_without_bind_res() { second_fetch := stmt.fetch_stmt()! assert second_fetch == 100 } + +fn test_stmt_prepare_returns_mysql_error_code() { + $if !network ? { + eprintln('> Skipping test ${@FN}, since `-d network` is not passed.') + eprintln('> This test requires a working mysql server running on localhost.') + return + } + config := mysql.Config{ + host: '127.0.0.1' + port: 3306 + username: 'root' + password: '' + dbname: 'mysql' + } + + mut db := mysql.connect(config)! + defer { + db.close() or {} + } + + mut stmt := db.init_stmt('insert into missing_stmt_errno_test (value) values (?)') + defer { + stmt.close() or {} + } + stmt.prepare() or { + assert err.code() == 1146 + return + } + assert false +} + +fn test_prepare_execute_returns_mysql_error_code() { + $if !network ? { + eprintln('> Skipping test ${@FN}, since `-d network` is not passed.') + eprintln('> This test requires a working mysql server running on localhost.') + return + } + config := mysql.Config{ + host: '127.0.0.1' + port: 3306 + username: 'root' + password: '' + dbname: 'mysql' + } + + mut db := mysql.connect(config)! + defer { + db.close() or {} + } + + db.exec('drop table if exists stmt_handle_errno_test')! + db.exec('create table if not exists stmt_handle_errno_test ( + id INT PRIMARY KEY AUTO_INCREMENT, + value VARCHAR(255) NOT NULL UNIQUE + )')! + db.exec_param('insert into stmt_handle_errno_test (value) values (?)', 'duplicate')! + + stmt := db.prepare('insert into stmt_handle_errno_test (value) values (?)')! + defer { + stmt.close() + } + stmt.execute(['duplicate']) or { + assert err.code() == 1062 + return + } + assert false +} diff --git a/vlib/db/mysql/stmt.c.v b/vlib/db/mysql/stmt.c.v index ae2ce2082..7ec647336 100644 --- a/vlib/db/mysql/stmt.c.v +++ b/vlib/db/mysql/stmt.c.v @@ -53,6 +53,7 @@ fn C.mysql_stmt_execute(&C.MYSQL_STMT) i32 fn C.mysql_stmt_close(&C.MYSQL_STMT) bool fn C.mysql_stmt_free_result(&C.MYSQL_STMT) bool fn C.mysql_stmt_error(&C.MYSQL_STMT) &char +fn C.mysql_stmt_errno(&C.MYSQL_STMT) i32 fn C.mysql_stmt_result_metadata(&C.MYSQL_STMT) &C.MYSQL_RES fn C.mysql_stmt_field_count(&C.MYSQL_STMT) u16 @@ -166,12 +167,23 @@ pub fn (stmt Stmt) close() ! { } fn (stmt Stmt) get_error_msg() string { - return unsafe { cstring_to_vstring(&char(C.mysql_stmt_error(stmt.stmt))) } + return get_stmt_error_msg(stmt.stmt) } -// error returns a proper V error with a human readable description, given the error code returned by MySQL -pub fn (stmt Stmt) error(code int) IError { +fn (stmt Stmt) get_error_code() int { + return get_stmt_errno(stmt.stmt) +} + +// error returns a proper V error with a human readable description, +// given the fallback status code returned by the MySQL statement API. +pub fn (stmt Stmt) error(fallback_code int) IError { msg := stmt.get_error_msg() + stmt_code := stmt.get_error_code() + code := if stmt_code != 0 { + stmt_code + } else { + fallback_code + } return &SQLError{ msg: '${msg} (${code}) (${stmt.query})' diff --git a/vlib/db/mysql/utils.c.v b/vlib/db/mysql/utils.c.v index 319927a0d..ea48daa3b 100644 --- a/vlib/db/mysql/utils.c.v +++ b/vlib/db/mysql/utils.c.v @@ -10,6 +10,16 @@ fn get_errno(conn &C.MYSQL) int { return C.mysql_errno(conn) } +// get_stmt_error_msg returns error message from a MySQL statement instance. +fn get_stmt_error_msg(stmt &C.MYSQL_STMT) string { + return unsafe { cstring_to_vstring(&char(C.mysql_stmt_error(stmt))) } +} + +// get_stmt_errno returns error number from a MySQL statement instance. +fn get_stmt_errno(stmt &C.MYSQL_STMT) int { + return C.mysql_stmt_errno(stmt) +} + // resolve_nil_str returns an empty string if passed value is a nil pointer. fn resolve_nil_str(ptr &u8) string { if isnil(ptr) { -- 2.39.5