db.mysql: add ability to prepare and execute statements separately (#20146)

This commit is contained in:
jacksonmowry 2023-12-11 11:34:20 +00:00 committed by GitHub
parent e6a30dc4ef
commit d86a11c257
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 117 additions and 13 deletions

View File

@ -90,6 +90,7 @@ const skip_test_files = [
'vlib/context/onecontext/onecontext_test.v', // backtrace_symbols is missing
'vlib/db/mysql/mysql_orm_test.v', // mysql not installed
'vlib/db/mysql/mysql_test.v', // mysql not installed
'vlib/db/mysql/prepared_stmt_test.v', // mysql not installed
'vlib/db/pg/pg_orm_test.v', // pg not installed
]
// These tests are too slow to be run in the CI on each PR/commit

View File

@ -345,6 +345,33 @@ pub fn (db &DB) exec_none(query string) int {
// exec_param_many executes the `query` with parameters provided as `?`'s in the query
// It returns either the full result set, or an error on failure
pub fn (db &DB) exec_param_many(query string, params []string) ![]Row {
stmt := db.prepare(query)!
defer {
stmt.close()
}
rows := stmt.execute(params)!
return rows
}
// exec_param executes the `query` with one parameter provided as an `?` in the query
// It returns either the full result set, or an error on failure
pub fn (db &DB) exec_param(query string, param string) ![]Row {
return db.exec_param_many(query, [param])!
}
// A StmtHandle is created through prepare, it will be bound
// to one DB connection and will become unusable if the connection
// is closed
pub struct StmtHandle {
stmt &C.MYSQL_STMT = &C.MYSQL_STMT(unsafe { nil })
db DB
}
// prepare takes in a query string, returning a StmtHandle
// that can then be used to execute the query as many times
// as needed, which must be closed manually by the user
// Placeholders are represented by `?`
pub fn (db &DB) prepare(query string) !StmtHandle {
stmt := C.mysql_stmt_init(db.conn)
if stmt == unsafe { nil } {
db.throw_mysql_error()!
@ -355,6 +382,19 @@ pub fn (db &DB) exec_param_many(query string, params []string) ![]Row {
db.throw_mysql_error()!
}
return StmtHandle{
stmt: stmt
db: DB{
conn: db.conn
}
}
}
// execute takes in an array of params that will be bound to the statement,
// followed by it's execution
// Returns an array of Rows, which will be empty if nothing is returned
// from the query, or possibly an error value
pub fn (stmt &StmtHandle) execute(params []string) ![]Row {
mut bind_params := []C.MYSQL_BIND{}
for param in params {
bind := C.MYSQL_BIND{
@ -366,17 +406,22 @@ pub fn (db &DB) exec_param_many(query string, params []string) ![]Row {
bind_params << bind
}
mut response := C.mysql_stmt_bind_param(stmt, unsafe { &C.MYSQL_BIND(bind_params.data) })
mut response := C.mysql_stmt_bind_param(stmt.stmt, unsafe { &C.MYSQL_BIND(bind_params.data) })
if response == true {
db.throw_mysql_error()!
stmt.db.throw_mysql_error()!
}
code = C.mysql_stmt_execute(stmt)
mut code := C.mysql_stmt_execute(stmt.stmt)
if code != 0 {
db.throw_mysql_error()!
stmt.db.throw_mysql_error()!
}
query_metadata := C.mysql_stmt_result_metadata(stmt)
query_metadata := C.mysql_stmt_result_metadata(stmt.stmt)
// If the query returns no metadata we have no data to return
// This happens in insert queries
if query_metadata == unsafe { nil } {
return []Row{}
}
num_cols := C.mysql_num_fields(query_metadata)
mut length := []u32{len: num_cols}
@ -392,9 +437,9 @@ pub fn (db &DB) exec_param_many(query string, params []string) ![]Row {
}
mut rows := []Row{}
response = C.mysql_stmt_bind_result(stmt, unsafe { &C.MYSQL_BIND(binds.data) })
response = C.mysql_stmt_bind_result(stmt.stmt, unsafe { &C.MYSQL_BIND(binds.data) })
for {
code = C.mysql_stmt_fetch(stmt)
code = C.mysql_stmt_fetch(stmt.stmt)
if code == mysql_no_data {
break
}
@ -405,20 +450,19 @@ pub fn (db &DB) exec_param_many(query string, params []string) ![]Row {
data := unsafe { malloc(l) }
binds[i].buffer = data
binds[i].buffer_length = l
code = C.mysql_stmt_fetch_column(stmt, unsafe { &binds[i] }, i, 0)
code = C.mysql_stmt_fetch_column(stmt.stmt, unsafe { &binds[i] }, i, 0)
row.vals << unsafe { data.vstring() }
}
rows << row
}
C.mysql_stmt_close(stmt)
return rows
}
// exec_param executes the `query` with one parameter provided as an `?` in the query
// It returns either the full result set, or an error on failure
pub fn (db &DB) exec_param(query string, param string) ![]Row {
return db.exec_param_many(query, [param])!
// close acts on a StmtHandle to close the mysql Stmt
// meaning it is no longer available for use
pub fn (stmt &StmtHandle) close() {
C.mysql_stmt_close(stmt.stmt)
}
@[inline]

View File

@ -28,6 +28,8 @@ fn test_mysql() {
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')!
@ -69,6 +71,9 @@ fn test_mysql() {
mysql.Row{
vals: ['4', 'blaze']
},
mysql.Row{
vals: ['5', 'Hi']
},
]
response = db.exec_param('select * from users where username = ?', 'blaze')!

View File

@ -0,0 +1,54 @@
import db.mysql
fn test_prep() {
config := mysql.Config{
host: '127.0.0.1'
port: 3306
username: 'root'
password: ''
dbname: 'mysql'
}
db := mysql.connect(config)!
mut response := db.exec('drop table if exists test')!
assert response == []mysql.Row{}
response = db.exec('create table if not exists test (
id INT PRIMARY KEY AUTO_INCREMENT,
value TEXT)')!
assert response == []mysql.Row{}
stmt := db.prepare('insert into test (value) values (?)')!
defer {
stmt.close()
}
names := ['jackson', 'hello', 'Disney', 'Marz', 'Bailey', 'Claxton']
for name in names {
response = stmt.execute([name])!
assert response == []mysql.Row{}
}
response = db.exec_param_many('select * from test', [''])!
assert response == [
mysql.Row{
vals: ['1', 'jackson']
},
mysql.Row{
vals: ['2', 'hello']
},
mysql.Row{
vals: ['3', 'Disney']
},
mysql.Row{
vals: ['4', 'Marz']
},
mysql.Row{
vals: ['5', 'Bailey']
},
mysql.Row{
vals: ['6', 'Claxton']
},
]
}