From 7039081d66b63e5c914b640d93e620889a18a693 Mon Sep 17 00:00:00 2001 From: kbkpbot Date: Fri, 27 Jun 2025 07:25:13 +0800 Subject: [PATCH] db: modify mysql/pg/sqlite interface for pool working (#24780) --- examples/database/mysql.v | 2 +- examples/database/mysql_pool.v | 159 +++++++++++++++++++++++++++++++++ examples/database/orm.v | 8 +- vlib/db/mysql/mysql.c.v | 12 ++- vlib/db/mysql/mysql_orm_test.v | 2 +- vlib/db/mysql/mysql_test.v | 5 ++ vlib/db/mysql/pool.v | 2 +- vlib/db/pg/pg.c.v | 12 ++- vlib/db/pg/pg_double_test.v | 2 +- vlib/db/pg/pg_orm_test.v | 2 +- vlib/db/pg/pg_test.v | 8 +- vlib/db/pg/pool.v | 2 +- vlib/db/sqlite/sqlite.c.v | 12 ++- vlib/db/sqlite/sqlite_test.v | 1 + vlib/pool/README.md | 6 +- 15 files changed, 214 insertions(+), 21 deletions(-) create mode 100644 examples/database/mysql_pool.v diff --git a/examples/database/mysql.v b/examples/database/mysql.v index 8ba007ca6d..7ddc167af4 100644 --- a/examples/database/mysql.v +++ b/examples/database/mysql.v @@ -13,5 +13,5 @@ fn main() { for row in res.rows() { println(row.vals.join(', ')) } - conn.close() + conn.close()! } diff --git a/examples/database/mysql_pool.v b/examples/database/mysql_pool.v new file mode 100644 index 0000000000..75b7d9b2b8 --- /dev/null +++ b/examples/database/mysql_pool.v @@ -0,0 +1,159 @@ +// vtest build: !(macos || windows) +import db.mysql +import pool +import time + +// Define your connection factory function +fn create_conn() !&pool.ConnectionPoolable { + config := mysql.Config{ + host: '127.0.0.1' + port: 3306 + username: 'root' + password: '12345678' + dbname: 'mysql' + } + db := mysql.connect(config)! + return &db +} + +fn main() { + // Configure pool parameters + config := pool.ConnectionPoolConfig{ + max_conns: 50 + min_idle_conns: 5 + max_lifetime: 2 * time.hour + idle_timeout: 30 * time.minute + get_timeout: 5 * time.second + } + + // Create connection pool + mut my_pool := pool.new_connection_pool(create_conn, config)! + defer { + // When application exits + my_pool.close() + } + + // Acquire connection + mut conn := my_pool.get()! + defer { + // Return connection to pool + my_pool.put(conn) or { println(err) } + } + + // Convert `conn` to a `mysql.DB` object + mut db := conn as mysql.DB + + assert db.validate()! + + mut response := db.exec('drop table if exists users')! + assert response == []mysql.Row{} + + response = db.exec('create table if not exists users ( + id INT PRIMARY KEY AUTO_INCREMENT, + username TEXT, + last_name TEXT NULL DEFAULT NULL + )')! + assert response == []mysql.Row{} + + mut result_code := db.exec_none('insert into users (username) values ("jackson")') + assert result_code == 0 + result_code = db.exec_none('insert into users (username) values ("shannon")') + assert result_code == 0 + result_code = db.exec_none('insert into users (username) values ("bailey")') + assert result_code == 0 + result_code = db.exec_none('insert into users (username) values ("blaze")') + assert result_code == 0 + rows := db.exec_param('insert into users (username) values (?)', 'Hi')! + assert rows == []mysql.Row{} + + // Regression testing to ensure the query and exec return the same values + res := db.query('select * from users')! + response = res.rows() + assert response[0].vals[1] == 'jackson' + response = db.exec('select * from users')! + assert response[0].vals[1] == 'jackson' + + response = db.exec('select * from users where id = 400')! + assert response.len == 0 + + single_row := db.exec_one('select * from users')! + assert single_row.vals[1] == 'jackson' + + response = db.exec_param_many('select * from users where username = ?', [ + 'jackson', + ])! + assert response[0] == mysql.Row{ + vals: ['1', 'jackson', ''] + } + + response = db.exec_param_many('select * from users where username = ? and id = ?', + ['bailey', '3'])! + assert response[0] == mysql.Row{ + vals: ['3', 'bailey', ''] + } + + response = db.exec_param_many('select * from users', [''])! + assert response == [ + mysql.Row{ + vals: ['1', 'jackson', ''] + }, + mysql.Row{ + vals: ['2', 'shannon', ''] + }, + mysql.Row{ + vals: ['3', 'bailey', ''] + }, + mysql.Row{ + vals: ['4', 'blaze', ''] + }, + mysql.Row{ + vals: ['5', 'Hi', ''] + }, + ] + + response = db.exec_param('select * from users where username = ?', 'blaze')! + assert response[0] == mysql.Row{ + vals: ['4', 'blaze', ''] + } + + // transaction test + // turn off `autocommit` first + db.autocommit(false)! + // begin a new transaction + db.begin()! + result_code = db.exec_none('insert into users (username) values ("tom")') + assert result_code == 0 + // make a savepoint + db.savepoint('savepoint1')! + result_code = db.exec_none('insert into users (username) values ("kitty")') + assert result_code == 0 + // rollback to `savepoint1` + db.rollback_to('savepoint1')! + result_code = db.exec_none('insert into users (username) values ("mars")') + assert result_code == 0 + db.commit()! + response = db.exec_param_many('select * from users', [''])! + assert response == [ + mysql.Row{ + vals: ['1', 'jackson', ''] + }, + mysql.Row{ + vals: ['2', 'shannon', ''] + }, + mysql.Row{ + vals: ['3', 'bailey', ''] + }, + mysql.Row{ + vals: ['4', 'blaze', ''] + }, + mysql.Row{ + vals: ['5', 'Hi', ''] + }, + mysql.Row{ + vals: ['6', 'tom', ''] + }, + mysql.Row{ + vals: ['8', 'mars', ''] + }, + ] +} diff --git a/examples/database/orm.v b/examples/database/orm.v index 7b4b5c0a4a..b2655bb9d3 100644 --- a/examples/database/orm.v +++ b/examples/database/orm.v @@ -110,7 +110,7 @@ fn msql_array() ! { sql db { drop table Parent } or {} - db.close() + db.close() or {} } db.query('drop table if exists Parent')! @@ -144,7 +144,7 @@ fn psql_array() ! { mut db := pg.connect(host: pg_host, user: pg_user, password: pg_pass, dbname: pg_db)! defer { db.exec_one('drop table if exists "Parent", "Child"') or { eprintln(err) } - db.close() + db.close() or {} } db.exec_one('drop table if exists "Parent", "Child"') or { eprintln(err) } @@ -219,7 +219,7 @@ fn msql() ! { defer { conn.query('DROP TABLE IF EXISTS Module') or { eprintln(err) } conn.query('DROP TABLE IF EXISTS User') or { eprintln(err) } - conn.close() + conn.close() or {} } conn.query('DROP TABLE IF EXISTS Module') or { eprintln(err) } conn.query('DROP TABLE IF EXISTS User') or { eprintln(err) } @@ -253,7 +253,7 @@ fn psql() ! { mut db := pg.connect(host: pg_host, user: pg_user, password: pg_pass, dbname: pg_db)! defer { db.exec_one('drop table if exists "modules", "User"') or { eprintln(err) } - db.close() + db.close() or {} } db.exec_one('drop table if exists "modules", "User"') or { eprintln(err) } sql db { diff --git a/vlib/db/mysql/mysql.c.v b/vlib/db/mysql/mysql.c.v index 31d19a54b3..b31ed76058 100644 --- a/vlib/db/mysql/mysql.c.v +++ b/vlib/db/mysql/mysql.c.v @@ -330,12 +330,10 @@ pub fn (mut db DB) refresh(options u32) !bool { } // reset resets the connection, and clear the session state. -pub fn (mut db DB) reset() !bool { +pub fn (mut db DB) reset() ! { if C.mysql_reset_connection(db.conn) != 0 { db.throw_mysql_error()! } - - return true } // ping pings a server connection, or tries to reconnect if the connection @@ -348,8 +346,14 @@ pub fn (mut db DB) ping() !bool { return true } +// validate pings a server connection, or tries to reconnect if the connection +// has gone down. +pub fn (mut db DB) validate() !bool { + return db.ping()! +} + // close closes the connection. -pub fn (mut db DB) close() { +pub fn (mut db DB) close() ! { C.mysql_close(db.conn) } diff --git a/vlib/db/mysql/mysql_orm_test.v b/vlib/db/mysql/mysql_orm_test.v index cb999b4b02..968a5113f3 100644 --- a/vlib/db/mysql/mysql_orm_test.v +++ b/vlib/db/mysql/mysql_orm_test.v @@ -58,7 +58,7 @@ fn test_mysql_orm() { dbname: 'mysql' )! defer { - db.close() + db.close() or {} } table := orm.Table{ name: 'Test' diff --git a/vlib/db/mysql/mysql_test.v b/vlib/db/mysql/mysql_test.v index 6869f9db76..d6c7abf149 100644 --- a/vlib/db/mysql/mysql_test.v +++ b/vlib/db/mysql/mysql_test.v @@ -16,6 +16,11 @@ fn test_mysql() { } mut db := mysql.connect(config)! + defer { + db.close() or {} + } + + assert db.validate()! mut response := db.exec('drop table if exists users')! assert response == []mysql.Row{} diff --git a/vlib/db/mysql/pool.v b/vlib/db/mysql/pool.v index 648921fb8f..0431f2361f 100644 --- a/vlib/db/mysql/pool.v +++ b/vlib/db/mysql/pool.v @@ -33,6 +33,6 @@ pub fn (mut pool ConnectionPool) release(conn DB) { pub fn (mut pool ConnectionPool) close() { for _ in 0 .. pool.connections.len { mut conn := <-pool.connections or { break } - conn.close() + conn.close() or { break } } } diff --git a/vlib/db/pg/pg.c.v b/vlib/db/pg/pg.c.v index 7fd5976157..002725c55f 100644 --- a/vlib/db/pg/pg.c.v +++ b/vlib/db/pg/pg.c.v @@ -224,7 +224,7 @@ fn res_to_rows(res voidptr) []Row { } // close frees the underlying resource allocated by the database connection -pub fn (db &DB) close() { +pub fn (db &DB) close() ! { C.PQfinish(db.conn) } @@ -510,3 +510,13 @@ pub fn (db &DB) savepoint(savepoint string) ! { return error('pg exec error: "${e}"') } } + +// validate checks if the connection is still usable +pub fn (db &DB) validate() !bool { + db.exec_one('SELECT 1')! + return true +} + +// reset returns the connection to initial state for reuse +pub fn (db &DB) reset() ! { +} diff --git a/vlib/db/pg/pg_double_test.v b/vlib/db/pg/pg_double_test.v index 485e9ff28b..4ab7ef8e73 100644 --- a/vlib/db/pg/pg_double_test.v +++ b/vlib/db/pg/pg_double_test.v @@ -19,7 +19,7 @@ fn test_float_field() { conn := 'host=localhost user=postgres password=12345678' // insert own connection string db := pg.connect_with_conninfo(conn)! defer { - db.close() + db.close() or {} } sql db { diff --git a/vlib/db/pg/pg_orm_test.v b/vlib/db/pg/pg_orm_test.v index 391a89b548..8eece80dd7 100644 --- a/vlib/db/pg/pg_orm_test.v +++ b/vlib/db/pg/pg_orm_test.v @@ -57,7 +57,7 @@ fn test_pg_orm() { ) or { panic(err) } defer { - db.close() + db.close() or {} } table := orm.Table{ name: 'Test' diff --git a/vlib/db/pg/pg_test.v b/vlib/db/pg/pg_test.v index 6bfa0e3828..a52aa36e56 100644 --- a/vlib/db/pg/pg_test.v +++ b/vlib/db/pg/pg_test.v @@ -12,9 +12,11 @@ fn test_large_exec() { db := pg.connect(pg.Config{ user: 'postgres', password: '12345678', dbname: 'postgres' })! defer { - db.close() + db.close() or {} } + assert db.validate()! + rows := db.exec(' SELECT ischema.table_schema, c.relname, a.attname, t.typname, t.typalign, t.typlen FROM pg_class c @@ -38,7 +40,7 @@ fn test_prepared() { } db := pg.connect(pg.Config{ user: 'postgres', password: '12345678', dbname: 'postgres' })! defer { - db.close() + db.close() or {} } db.prepare('test_prepared', 'SELECT NOW(), $1 AS NAME', 1) or { panic(err) } @@ -57,7 +59,7 @@ fn test_transaction() { db := pg.connect(pg.Config{ user: 'postgres', password: '12345678', dbname: 'postgres' })! defer { - db.close() + db.close() or {} } db.exec('drop table if exists users')! db.exec('create table if not exists users ( diff --git a/vlib/db/pg/pool.v b/vlib/db/pg/pool.v index e8a32591d2..73873cc8eb 100644 --- a/vlib/db/pg/pool.v +++ b/vlib/db/pg/pool.v @@ -33,6 +33,6 @@ pub fn (mut pool ConnectionPool) release(conn DB) { pub fn (mut pool ConnectionPool) close() { for _ in 0 .. pool.connections.len { conn := <-pool.connections or { break } - conn.close() + conn.close() or { break } } } diff --git a/vlib/db/sqlite/sqlite.c.v b/vlib/db/sqlite/sqlite.c.v index 73425618ca..2a294304a3 100644 --- a/vlib/db/sqlite/sqlite.c.v +++ b/vlib/db/sqlite/sqlite.c.v @@ -150,7 +150,7 @@ pub fn connect(path string) !DB { // close Closes the DB. // TODO: For all functions, determine whether the connection is // closed first, and determine what to do if it is -pub fn (mut db DB) close() !bool { +pub fn (mut db DB) close() ! { code := C.sqlite3_close(db.conn) if code == 0 { db.is_open = false @@ -160,7 +160,6 @@ pub fn (mut db DB) close() !bool { code: code } } - return true // successfully closed } // Only for V ORM @@ -500,3 +499,12 @@ pub fn (mut db DB) rollback_to(savepoint string) ! { } db.exec('ROLLBACK TO ${savepoint};')! } + +// reset returns the connection to initial state for reuse +pub fn (mut db DB) reset() ! { +} + +// validate checks if the connection is still usable +pub fn (mut db DB) validate() !bool { + return db.exec_none('SELECT 1') == 100 +} diff --git a/vlib/db/sqlite/sqlite_test.v b/vlib/db/sqlite/sqlite_test.v index 1b8019db35..c60122fdf8 100644 --- a/vlib/db/sqlite/sqlite_test.v +++ b/vlib/db/sqlite/sqlite_test.v @@ -37,6 +37,7 @@ fn test_sqlite() { } mut db := sqlite.connect(':memory:') or { panic(err) } assert db.is_open + assert db.validate()! db.exec('drop table if exists users')! db.exec("create table users (id integer primary key, name text default '', last_name text null default null);")! db.exec("insert into users (name) values ('Sam')")! diff --git a/vlib/pool/README.md b/vlib/pool/README.md index 1e75784550..3a2377f7a9 100644 --- a/vlib/pool/README.md +++ b/vlib/pool/README.md @@ -53,8 +53,12 @@ mut my_pool := pool.new_connection_pool(create_conn, config)! // Acquire connection mut conn := my_pool.get()! -// Use connection +// Convert `conn` to a `mysql.DB` object +mut db := conn as mysql.DB + +// Use connection `db` // ... your operations ... +// db.exec() // Return connection to pool my_pool.put(conn)!