From d86a11c2570767c890a9dbac99f0a971b1ba84c3 Mon Sep 17 00:00:00 2001 From: jacksonmowry <96317858+jacksonmowry@users.noreply.github.com> Date: Mon, 11 Dec 2023 11:34:20 +0000 Subject: [PATCH] db.mysql: add ability to prepare and execute statements separately (#20146) --- cmd/tools/vtest-self.v | 1 + vlib/db/mysql/mysql.c.v | 70 ++++++++++++++++++++++++------ vlib/db/mysql/mysql_test.v | 5 +++ vlib/db/mysql/prepared_stmt_test.v | 54 +++++++++++++++++++++++ 4 files changed, 117 insertions(+), 13 deletions(-) create mode 100644 vlib/db/mysql/prepared_stmt_test.v diff --git a/cmd/tools/vtest-self.v b/cmd/tools/vtest-self.v index db0824e345..aea49b8bed 100644 --- a/cmd/tools/vtest-self.v +++ b/cmd/tools/vtest-self.v @@ -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 diff --git a/vlib/db/mysql/mysql.c.v b/vlib/db/mysql/mysql.c.v index f61fdaedc6..092aa2596b 100644 --- a/vlib/db/mysql/mysql.c.v +++ b/vlib/db/mysql/mysql.c.v @@ -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] diff --git a/vlib/db/mysql/mysql_test.v b/vlib/db/mysql/mysql_test.v index 86f3a02a82..a0d466b9d8 100644 --- a/vlib/db/mysql/mysql_test.v +++ b/vlib/db/mysql/mysql_test.v @@ -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')! diff --git a/vlib/db/mysql/prepared_stmt_test.v b/vlib/db/mysql/prepared_stmt_test.v new file mode 100644 index 0000000000..30e814c65e --- /dev/null +++ b/vlib/db/mysql/prepared_stmt_test.v @@ -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'] + }, + ] +}