From 3bac2c72359ca58412f31490914b16f20bdaf833 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Tue, 14 Apr 2026 12:45:24 +0300 Subject: [PATCH] db: db.pg and db.mysql consistency (fixes #24035) --- vlib/db/README.md | 11 ++++++ vlib/db/mysql/mysql.c.v | 32 ++++++++++++++++-- vlib/db/mysql_consistency_test.v | 34 +++++++++++++++++++ vlib/db/pg/pg.c.v | 43 +++++++++++++++++++++--- vlib/db/pg_sqlite_consistency_test.v | 50 ++++++++++++++++++++++++++++ vlib/db/sqlite/sqlite.c.v | 15 +++++++++ 6 files changed, 179 insertions(+), 6 deletions(-) create mode 100644 vlib/db/mysql_consistency_test.v create mode 100644 vlib/db/pg_sqlite_consistency_test.v diff --git a/vlib/db/README.md b/vlib/db/README.md index ccaf9388a..a4030bfea 100644 --- a/vlib/db/README.md +++ b/vlib/db/README.md @@ -2,3 +2,14 @@ `db` is a namespace that contains several useful modules for operating with databases (SQLite, MySQL, MSQL, etc.) + +## Cross-driver consistency helpers + +`db.pg` and `db.mysql` accept both `user` and `username` in their `Config` structs. + +`db.pg`, `db.mysql`, and `db.sqlite` rows expose `row.val(index)` and `row.values()` +for direct string access. In `db.pg`, SQL `NULL` remains available through +`row.val_opt(index)`. + +`db.pg`, `db.mysql`, and `db.sqlite` also expose `exec_param2(...)` as a convenience +wrapper around their parameterized query helpers. diff --git a/vlib/db/mysql/mysql.c.v b/vlib/db/mysql/mysql.c.v index b3dcfc730..fee2c3773 100644 --- a/vlib/db/mysql/mysql.c.v +++ b/vlib/db/mysql/mysql.c.v @@ -59,6 +59,7 @@ pub struct Config { pub mut: host string = '127.0.0.1' port u32 = 3306 + user string username string password string dbname string @@ -72,11 +73,33 @@ pub mut: ssl_cipher string } +// connection_user returns the configured username, accepting both `user` and `username`. +pub fn (config Config) connection_user() !string { + if config.user != '' && config.username != '' && config.user != config.username { + return error('db.mysql: Config.user and Config.username must match when both are set') + } + if config.username != '' { + return config.username + } + return config.user +} + +// val returns the value at `index`. +pub fn (row Row) val(index int) string { + return row.vals[index] +} + +// values returns all row values. +pub fn (row Row) values() []string { + return row.vals.clone() +} + // connect attempts to establish a connection to a MySQL server. pub fn connect(config Config) !DB { mut db := DB{ conn: C.mysql_init(0) } + username := config.connection_user()! if config.flag.has(.client_ssl) { if config.ssl_key.len > 0 { @@ -96,8 +119,8 @@ pub fn connect(config Config) !DB { } } - connection := C.mysql_real_connect(db.conn, config.host.str, config.username.str, - config.password.str, config.dbname.str, config.port, 0, config.flag) + connection := C.mysql_real_connect(db.conn, config.host.str, username.str, config.password.str, + config.dbname.str, config.port, 0, config.flag) if isnil(connection) { db.throw_mysql_error()! @@ -493,6 +516,11 @@ pub fn (db &DB) exec_param(query string, param string) ![]Row { return db.exec_param_many(query, [param])! } +// exec_param2 executes the `query` with two parameters provided as `?` placeholders. +pub fn (db &DB) exec_param2(query string, param string, param2 string) ![]Row { + return db.exec_param_many(query, [param, param2])! +} + // A StmtHandle is created through prepare, it will be bound // to one DB connection and will become unusable if the connection // is closed diff --git a/vlib/db/mysql_consistency_test.v b/vlib/db/mysql_consistency_test.v new file mode 100644 index 000000000..9ff929e87 --- /dev/null +++ b/vlib/db/mysql_consistency_test.v @@ -0,0 +1,34 @@ +// vtest build: started_mysqld? +module main + +import db.mysql + +fn test_mysql_connection_user_aliases() { + assert mysql.Config{ + user: 'alice' + }.connection_user()! == 'alice' + assert mysql.Config{ + username: 'alice' + }.connection_user()! == 'alice' + assert mysql.Config{ + user: 'alice' + username: 'alice' + }.connection_user()! == 'alice' + if _ := mysql.Config{ + user: 'alice' + username: 'bob' + }.connection_user() + { + assert false + } else { + assert err.msg().contains('must match') + } +} + +fn test_mysql_row_value_helpers() { + mysql_row := mysql.Row{ + vals: ['hello', ''] + } + assert mysql_row.val(0) == 'hello' + assert mysql_row.values() == ['hello', ''] +} diff --git a/vlib/db/pg/pg.c.v b/vlib/db/pg/pg.c.v index d39b9995d..f9391d59d 100644 --- a/vlib/db/pg/pg.c.v +++ b/vlib/db/pg/pg.c.v @@ -69,6 +69,28 @@ pub mut: vals []?string } +// val returns the value at `index`, flattening SQL NULL to an empty string. +pub fn (row Row) val(index int) string { + if val := row.vals[index] { + return val + } + return '' +} + +// values returns all row values, flattening SQL NULL to empty strings. +pub fn (row Row) values() []string { + mut values := []string{cap: row.vals.len} + for val in row.vals { + values << if value := val { value } else { '' } + } + return values +} + +// val_opt returns the raw optional value at `index`. +pub fn (row Row) val_opt(index int) ?string { + return row.vals[index] +} + pub struct RowNoNull { pub mut: vals []string @@ -93,6 +115,7 @@ pub: host string = 'localhost' port int = 5432 user string + username string password string dbname string } @@ -235,7 +258,18 @@ fn escape_conninfo_value(value string) string { return escaped.bytestr() } -fn (config Config) conninfo() string { +// connection_user returns the configured username, accepting both `user` and `username`. +pub fn (config Config) connection_user() !string { + if config.user != '' && config.username != '' && config.user != config.username { + return error('db.pg: Config.user and Config.username must match when both are set') + } + if config.user != '' { + return config.user + } + return config.username +} + +fn (config Config) conninfo() !string { mut parts := []string{cap: 5} if config.host != '' { parts << 'host=${escape_conninfo_value(config.host)}' @@ -243,8 +277,9 @@ fn (config Config) conninfo() string { if config.port > 0 { parts << 'port=${config.port}' } - if config.user != '' { - parts << 'user=${escape_conninfo_value(config.user)}' + user := config.connection_user()! + if user != '' { + parts << 'user=${escape_conninfo_value(user)}' } if config.dbname != '' { parts << 'dbname=${escape_conninfo_value(config.dbname)}' @@ -260,7 +295,7 @@ fn (config Config) conninfo() string { // a connection error when something goes wrong. // Empty fields are omitted so libpq defaults can still apply. pub fn connect(config Config) !DB { - return connect_with_conninfo(config.conninfo())! + return connect_with_conninfo(config.conninfo()!)! } // connect_with_conninfo makes a new connection to the database server using diff --git a/vlib/db/pg_sqlite_consistency_test.v b/vlib/db/pg_sqlite_consistency_test.v new file mode 100644 index 000000000..a0e7dd335 --- /dev/null +++ b/vlib/db/pg_sqlite_consistency_test.v @@ -0,0 +1,50 @@ +module main + +import db.pg +import db.sqlite + +fn test_pg_connection_user_aliases() { + assert pg.Config{ + user: 'alice' + }.connection_user()! == 'alice' + assert pg.Config{ + username: 'alice' + }.connection_user()! == 'alice' + assert pg.Config{ + user: 'alice' + username: 'alice' + }.connection_user()! == 'alice' + if _ := pg.Config{ + user: 'alice' + username: 'bob' + }.connection_user() + { + assert false + } else { + assert err.msg().contains('must match') + } +} + +fn test_pg_row_value_helpers() { + mut vals := []?string{} + vals << none + vals << 'hello' + vals << '' + row := pg.Row{ + vals: vals + } + assert row.val(0) == '' + assert row.val(1) == 'hello' + assert row.values() == ['', 'hello', ''] + assert row.val_opt(0) == none + assert row.val_opt(1) or { '' } == 'hello' +} + +fn test_sqlite_row_value_helpers() { + sqlite_row := sqlite.Row{ + vals: ['hello', ''] + names: ['first', 'second'] + } + assert sqlite_row.val(0) == 'hello' + assert sqlite_row.values() == ['hello', ''] +} diff --git a/vlib/db/sqlite/sqlite.c.v b/vlib/db/sqlite/sqlite.c.v index 9de08a498..804e7ceda 100644 --- a/vlib/db/sqlite/sqlite.c.v +++ b/vlib/db/sqlite/sqlite.c.v @@ -96,6 +96,16 @@ pub mut: names []string } +// val returns the value at `index`. +pub fn (row Row) val(index int) string { + return row.vals[index] +} + +// values returns all row values. +pub fn (row Row) values() []string { + return row.vals.clone() +} + // get_string returns the value for the given column name, or '' if the column is not found // or if the corresponding value index is out of range. pub fn (r &Row) get_string(col_name string) string { @@ -480,6 +490,11 @@ pub fn (db &DB) exec_param(query string, param string) ![]Row { return db.exec_param_many(query, [param]) } +// exec_param2 executes a query with two parameters provided as ? placeholders. +pub fn (db &DB) exec_param2(query string, param string, param2 string) ![]Row { + return db.exec_param_many(query, [param, param2]) +} + // create_table issues a "create table if not exists" command to the db. // It creates the table named 'table_name', with columns generated from 'columns' array. // The default columns type will be TEXT. -- 2.39.5