Initial project import
This commit is contained in:
commit
23f2d29e32
11957 files changed
+1754577
No files matched your search
@@ -0,0 +1,113 @@
|
||||
## Purpose
|
||||
|
||||
The db.mysql module can be used to develop software that connects to the popular open source
|
||||
MySQL or MariaDB database servers.
|
||||
|
||||
### Local setup of a development server
|
||||
|
||||
To run the mysql module tests, or if you want to just experiment, you can use the following
|
||||
command to start a development version of MySQL using docker:
|
||||
```sh
|
||||
docker run -p 3306:3306 --name some-mysql -e MYSQL_ROOT_PASSWORD=12345678 -d mysql:latest
|
||||
```
|
||||
The above command will start a server instance with the root password `12345678`,
|
||||
available to mysql client connections, on tcp port 3306.
|
||||
|
||||
You can test that it works by doing: `mysql -uroot -p12345678 -h127.0.0.1` .
|
||||
You should see a mysql shell (use `exit` to end the mysql client session).
|
||||
|
||||
Use `docker container stop some-mysql` to stop the server.
|
||||
|
||||
Use `docker container rm some-mysql` to remove it completely, after it is stopped.
|
||||
|
||||
### Installation of development dependencies
|
||||
|
||||
For Linux, you need to install `MySQL development` package and `pkg-config`.
|
||||
|
||||
For FreeBSD, you need to install the `mariadb118-client` package.
|
||||
|
||||
For OpenBSD, you need to install the `mariadb-client` package.
|
||||
|
||||
For Windows, install [the installer](https://dev.mysql.com/downloads/installer/) or extract the
|
||||
ZIP package, then copy the `include`, `lib`, and `bin` folders to
|
||||
`<V install directory>\thirdparty\mysql`.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
If a program that imports `db.mysql` exits right away on Windows before
|
||||
`fn main()` prints anything, Windows usually could not load `libmysql.dll`.
|
||||
Make sure that `thirdparty/mysql/bin` and `thirdparty/mysql/lib` are on `PATH`.
|
||||
If you still need a workaround, copy `libmysql.dll` next to the produced `.exe`.
|
||||
|
||||
One common sign of this problem is the process exit code `-1073741515`
|
||||
(`0xC0000135`).
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```v oksyntax
|
||||
import db.mysql
|
||||
|
||||
// Create connection
|
||||
config := mysql.Config{
|
||||
host: '127.0.0.1'
|
||||
port: 3306
|
||||
username: 'root'
|
||||
password: '12345678'
|
||||
dbname: 'mysql'
|
||||
}
|
||||
|
||||
// Connect to server
|
||||
mut db := mysql.connect(config)!
|
||||
// Do a query
|
||||
res := db.query('select * from users')!
|
||||
rows := res.rows()
|
||||
for row in rows {
|
||||
println(row.vals)
|
||||
}
|
||||
// Close the connection if needed
|
||||
db.close()
|
||||
```
|
||||
|
||||
## Concurrent Usage
|
||||
|
||||
Sharing one `mysql.DB` across threads now serializes connection-level queries safely.
|
||||
|
||||
For concurrent servers, prefer `mysql.new_connection_pool(...)` so requests do not share the same
|
||||
session and transaction state on one connection.
|
||||
|
||||
## Transaction
|
||||
|
||||
```v oksyntax
|
||||
import db.mysql
|
||||
|
||||
// Create connection
|
||||
config := mysql.Config{
|
||||
host: '127.0.0.1'
|
||||
port: 3306
|
||||
username: 'root'
|
||||
password: '12345678'
|
||||
dbname: 'mysql'
|
||||
}
|
||||
|
||||
mut db := mysql.connect(config)!
|
||||
// turn off `autocommit` first
|
||||
db.autocommit(false)!
|
||||
// begin a new transaction
|
||||
db.begin()!
|
||||
mut 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()!
|
||||
res := db.query('select * from users')!
|
||||
rows := res.rows()
|
||||
dump(rows)
|
||||
// Close the connection if needed
|
||||
db.close()
|
||||
```
|
||||
@@ -0,0 +1,181 @@
|
||||
module mysql
|
||||
|
||||
@[typedef]
|
||||
pub struct C.MYSQL {
|
||||
}
|
||||
|
||||
@[typedef]
|
||||
pub struct C.MYSQL_RES {
|
||||
}
|
||||
|
||||
@[typedef]
|
||||
pub struct C.MYSQL_FIELD {
|
||||
name &u8 // Name of column
|
||||
org_name &u8 // Original column name, if an alias
|
||||
table &u8 // Table of column if column was a field
|
||||
org_table &u8 // Org table name, if table was an alias
|
||||
db &u8 // Name of the database that the field comes from
|
||||
catalog &u8 // Catalog for table
|
||||
def &u8 // Default value (set by `mysql_list_fields`)
|
||||
length int // Width of column (create length)
|
||||
max_length int // Max width for selected set
|
||||
name_length u32
|
||||
org_name_length u32
|
||||
table_length u32
|
||||
org_table_length u32
|
||||
db_length u32
|
||||
catalog_length u32
|
||||
def_length u32
|
||||
flags u32 // Bit-flags that describe the field
|
||||
decimals u32 // Number of decimals in field
|
||||
charsetnr u32 // Character set
|
||||
type int // Type of field. See enums.v for types
|
||||
}
|
||||
|
||||
// C.mysql_init allocates or initializes a MYSQL object suitable for `mysql_real_connect()`.
|
||||
fn C.mysql_init(mysql &C.MYSQL) &C.MYSQL
|
||||
|
||||
// C.mysql_thread_init initializes thread-local client state for threads using the MySQL C API.
|
||||
fn C.mysql_thread_init() bool
|
||||
|
||||
// C.mysql_thread_end finalizes thread-local client state for threads using the MySQL C API.
|
||||
fn C.mysql_thread_end()
|
||||
|
||||
// C.mysql_real_connect attempts to establish a connection to a MySQL server running on `host`.
|
||||
fn C.mysql_real_connect(mysql &C.MYSQL, host &char, user &char, passwd &char, db &char, port u32, unix_socket &char,
|
||||
client_flag ConnectionFlag) &C.MYSQL
|
||||
|
||||
// C.mysql_query executes the SQL statement pointed to by the null-terminated string `stmt_str`.
|
||||
fn C.mysql_query(mysql &C.MYSQL, const_q charptr) i32
|
||||
|
||||
// C.mysql_use_result initiates a result set retrieval but does not actually read
|
||||
// the result set into the client like `mysql_store_result()` does.
|
||||
fn C.mysql_use_result(mysql &C.MYSQL)
|
||||
|
||||
// C.mysql_real_query executes the SQL statement pointed to by `stmt_str`,
|
||||
// a string length bytes long.
|
||||
fn C.mysql_real_query(mysql &C.MYSQL, q &u8, len u32) i32
|
||||
|
||||
// C.mysql_select_db causes the database specified by `db` to become
|
||||
// the default (current) database on the connection specified by mysql.
|
||||
fn C.mysql_select_db(mysql &C.MYSQL, db &u8) i32
|
||||
|
||||
// C.mysql_change_user changes the user and causes the database specified by `db` to become
|
||||
// the default (current) database on the connection specified by `mysql`.
|
||||
fn C.mysql_change_user(mysql &C.MYSQL, user &u8, password &u8, db &u8) bool
|
||||
|
||||
// C.mysql_affected_rows returns the number of rows changed, deleted,
|
||||
// or inserted by the last statement if it was an `UPDATE`, `DELETE`, or `INSERT`.
|
||||
fn C.mysql_affected_rows(mysql &C.MYSQL) u64
|
||||
|
||||
// C.mysql_options sets extra connect options and affects behavior for a connection.
|
||||
fn C.mysql_options(mysql &C.MYSQL, option i32, arg voidptr) i32
|
||||
|
||||
// C.mysql_get_option returns the current value of an option settable using `mysql_options()`.
|
||||
fn C.mysql_get_option(mysql &C.MYSQL, option i32, arg voidptr) i32
|
||||
|
||||
// C.mysql_list_tables returns a result set consisting of table names in the current database
|
||||
// that match the simple regular expression specified by the `wild` parameter.
|
||||
// `wild` may contain the wildcard characters `%` or `_`,
|
||||
// or may be a `NULL` pointer to match all tables.
|
||||
fn C.mysql_list_tables(mysql &C.MYSQL, wild &u8) &C.MYSQL_RES
|
||||
|
||||
// C.mysql_num_fields returns the number of columns in a result set.
|
||||
fn C.mysql_num_fields(res &C.MYSQL_RES) i32
|
||||
|
||||
// C.mysql_num_rows returns the number of rows in the result set.
|
||||
fn C.mysql_num_rows(res &C.MYSQL_RES) u64
|
||||
|
||||
// C.mysql_autocommit sets autocommit mode on if `mode` is 1, off if `mode` is 0.
|
||||
fn C.mysql_autocommit(mysql &C.MYSQL, mode bool) i32
|
||||
|
||||
// C.mysql_commit commits the current transaction.
|
||||
fn C.mysql_commit(mysql &C.MYSQL) i32
|
||||
|
||||
// C.mysql_rollback rollback the current transaction.
|
||||
fn C.mysql_rollback(mysql &C.MYSQL) i32
|
||||
|
||||
// C.mysql_refresh flush tables or caches, or resets replication server information.
|
||||
fn C.mysql_refresh(mysql &C.MYSQL, options u32) i32
|
||||
|
||||
// C.mysql_reset_connection resets the connection to clear the session state.
|
||||
fn C.mysql_reset_connection(mysql &C.MYSQL) i32
|
||||
|
||||
// C.mysql_ping checks whether the connection to the server is working.
|
||||
// Returns zero if the connection to the server is active. Nonzero if an error occurred.
|
||||
fn C.mysql_ping(mysql &C.MYSQL) i32
|
||||
|
||||
// C.mysql_store_result reads the entire result of a query to the client,
|
||||
// allocates a `MYSQL_RES` structure, and places the result into this structure.
|
||||
// It is a synchronous function.
|
||||
fn C.mysql_store_result(mysql &C.MYSQL) &C.MYSQL_RES
|
||||
|
||||
// C.mysql_more_results returns `true` if more result sets are available from the
|
||||
// previously executed multi-statement query.
|
||||
fn C.mysql_more_results(mysql &C.MYSQL) bool
|
||||
|
||||
// C.mysql_next_result advances to the next result set of a multi-statement query.
|
||||
// Returns `0` on success when another result set is available, `-1` when there
|
||||
// are no more result sets, and a positive value if an error occurred.
|
||||
fn C.mysql_next_result(mysql &C.MYSQL) i32
|
||||
|
||||
// C.mysql_fetch_row retrieves the next row of a result set.
|
||||
fn C.mysql_fetch_row(res &C.MYSQL_RES) &charptr
|
||||
|
||||
// C.mysql_fetch_fields returns an array of all `MYSQL_FIELD` structures for a result set.
|
||||
// Each structure provides the field definition for one column of the result set.
|
||||
fn C.mysql_fetch_fields(res &C.MYSQL_RES) &C.MYSQL_FIELD
|
||||
|
||||
// C.mysql_free_result frees the memory allocated for a result set by `mysql_store_result()`,
|
||||
// `mysql_use_result()`, `mysql_list_dbs()`, and so forth.
|
||||
fn C.mysql_free_result(res &C.MYSQL_RES)
|
||||
|
||||
// C.mysql_real_escape_string creates a legal SQL string for use in an SQL statement.
|
||||
fn C.mysql_real_escape_string(mysql &C.MYSQL, to &u8, from &u8, len u64) u64
|
||||
|
||||
// C.mysql_close closes a previously opened connection.
|
||||
fn C.mysql_close(sock &C.MYSQL)
|
||||
|
||||
// C.mysql_info retrieves a string providing information about the most recently executed statement.
|
||||
fn C.mysql_info(mysql &C.MYSQL) &u8
|
||||
|
||||
// C.mysql_get_host_info returns a string describing the type of connection in use,
|
||||
// including the server host name.
|
||||
fn C.mysql_get_host_info(mysql &C.MYSQL) &u8
|
||||
|
||||
// C.mysql_get_server_info returns a string that represents
|
||||
// the MySQL server version (for example, "8.0.33").
|
||||
fn C.mysql_get_server_info(mysql &C.MYSQL) &u8
|
||||
|
||||
// C.mysql_get_server_version returns an integer that represents the MySQL server version.
|
||||
// The value has the format `XYYZZ` where `X` is the major version,
|
||||
// `YY` is the release level (or minor version),
|
||||
// and `ZZ` is the sub-version within the release level:
|
||||
// `major_version*10000 + release_level*100 + sub_version`
|
||||
// For example, "8.0.33" is returned as 80033.
|
||||
fn C.mysql_get_server_version(mysql &C.MYSQL) u64
|
||||
|
||||
// C.mysql_get_client_version returns an integer that represents the MySQL client library version.
|
||||
// The value has the format `XYYZZ` where `X` is the major version,
|
||||
// `YY` is the release level (or minor version),
|
||||
// and `ZZ` is the sub-version within the release level:
|
||||
// `major_version*10000 + release_level*100 + sub_version`
|
||||
// For example, "8.0.33" is returned as 80033.
|
||||
fn C.mysql_get_client_version() u64
|
||||
|
||||
// C.mysql_get_client_info returns a string that represents
|
||||
// the MySQL client library version (for example, "8.0.33").
|
||||
fn C.mysql_get_client_info() &u8
|
||||
|
||||
// C.mysql_error returns a null-terminated string containing the error message
|
||||
// for the most recently invoked API function that failed.
|
||||
fn C.mysql_error(mysql &C.MYSQL) &u8
|
||||
|
||||
// C.mysql_errno returns the error code for the most recently invoked API function that can succeed or fail.
|
||||
fn C.mysql_errno(mysql &C.MYSQL) i32
|
||||
|
||||
// C.mysql_dump_debug_info instructs the server to write debugging information to the error log.
|
||||
fn C.mysql_dump_debug_info(mysql &C.MYSQL) i32
|
||||
|
||||
// C.mysql_debug does a `DBUG_PUSH` with the given string.
|
||||
fn C.mysql_debug(debug &u8)
|
||||
@@ -0,0 +1,34 @@
|
||||
module mysql
|
||||
|
||||
// Need to check if `mysqlclient` is not there and use `mariadb` as alternative
|
||||
// because newer system doesn't support mysql 8.0 as default.
|
||||
|
||||
$if $pkgconfig('mysqlclient') {
|
||||
#pkgconfig mysqlclient
|
||||
#include <mysql.h> # Please install the libmysqlclient-dev development headers
|
||||
} $else $if $pkgconfig('mariadb') {
|
||||
#pkgconfig mariadb
|
||||
$if openbsd {
|
||||
#include <mysql.h> # Please install the mariadb-client package for development headers
|
||||
} $else $if freebsd {
|
||||
#include <mysql.h> # Please install the mariadb118-client package for development headers
|
||||
} $else {
|
||||
#include <mysql.h> # Please install the libmariadb-dev development headers
|
||||
}
|
||||
} $else $if $pkgconfig('libmariadb') {
|
||||
#pkgconfig libmariadb
|
||||
#include <mysql.h> # Please install the mariadb client
|
||||
} $else {
|
||||
// Common fallback paths when pkg-config is unavailable.
|
||||
#flag linux -I$when_first_existing('/usr/include/mysql','/usr/include/mariadb','/usr/local/include/mysql','/usr/local/include/mariadb')
|
||||
#flag linux $when_first_existing('/usr/lib64/libmysqlclient.so','/usr/lib64/mysql/libmysqlclient.so','/usr/lib/libmysqlclient.so','/usr/lib/mysql/libmysqlclient.so','/usr/lib/x86_64-linux-gnu/libmysqlclient.so','/usr/lib/aarch64-linux-gnu/libmysqlclient.so','/usr/local/lib/libmysqlclient.so','/usr/local/mysql/lib/libmysqlclient.so','/usr/lib64/libmariadb.so','/usr/lib64/mysql/libmariadb.so','/usr/lib/libmariadb.so','/usr/lib/mysql/libmariadb.so','/usr/lib/x86_64-linux-gnu/libmariadb.so','/usr/lib/aarch64-linux-gnu/libmariadb.so','/usr/local/lib/libmariadb.so')
|
||||
#flag darwin -I$when_first_existing('/opt/homebrew/include/mysql','/opt/homebrew/include/mariadb','/opt/homebrew/opt/mysql-client/include/mysql','/opt/homebrew/opt/mariadb-connector-c/include/mariadb','/usr/local/include/mysql','/usr/local/include/mariadb','/usr/local/opt/mysql-client/include/mysql','/usr/local/opt/mariadb-connector-c/include/mariadb')
|
||||
#flag darwin $when_first_existing('/opt/homebrew/lib/libmysqlclient.dylib','/opt/homebrew/opt/mysql-client/lib/libmysqlclient.dylib','/usr/local/lib/libmysqlclient.dylib','/usr/local/opt/mysql-client/lib/libmysqlclient.dylib','/opt/homebrew/lib/libmariadb.dylib','/opt/homebrew/opt/mariadb-connector-c/lib/libmariadb.dylib','/usr/local/lib/libmariadb.dylib','/usr/local/opt/mariadb-connector-c/lib/libmariadb.dylib')
|
||||
#flag freebsd -I$when_first_existing('/usr/local/include/mysql','/usr/local/include/mariadb')
|
||||
#flag freebsd $when_first_existing('/usr/local/lib/libmysqlclient.so','/usr/local/lib/libmariadb.so')
|
||||
#flag openbsd -I$when_first_existing('/usr/local/include/mysql','/usr/local/include/mariadb')
|
||||
#flag openbsd $when_first_existing('/usr/local/lib/libmysqlclient.so','/usr/local/lib/libmariadb.so')
|
||||
#include <mysql.h> # Please install the mysql or mariadb development headers
|
||||
}
|
||||
|
||||
#include "@VMODROOT/vlib/db/mysql/mysql_compat.h"
|
||||
@@ -0,0 +1,12 @@
|
||||
module mysql
|
||||
|
||||
#flag windows -I@VEXEROOT/thirdparty/mysql/include
|
||||
|
||||
$if tinyc {
|
||||
#flag windows @VEXEROOT/thirdparty/mysql/lib/libmysql.dll
|
||||
} $else {
|
||||
#flag windows @VEXEROOT/thirdparty/mysql/lib/libmysql.lib
|
||||
}
|
||||
|
||||
#include <mysql.h> # Please install https://dev.mysql.com/downloads/installer/ , then copy the include, lib, and bin folders to thirdparty/mysql. Make sure thirdparty/mysql/bin and thirdparty/mysql/lib are on PATH, or copy libmysql.dll next to the produced .exe
|
||||
#include "@VMODROOT/vlib/db/mysql/mysql_compat.h"
|
||||
@@ -0,0 +1,26 @@
|
||||
module mysql
|
||||
|
||||
// MySQL refresh flags.
|
||||
// Docs: https://dev.mysql.com/doc/c-api/8.0/en/mysql-refresh.html
|
||||
// /usr/include/mysql/mysql_com.h
|
||||
pub const refresh_grant = u32(0x1)
|
||||
pub const refresh_log = u32(0x2)
|
||||
pub const refresh_tables = u32(0x4)
|
||||
pub const refresh_hosts = u32(0x8)
|
||||
pub const refresh_status = u32(0x10)
|
||||
pub const refresh_threads = u32(0x20)
|
||||
pub const refresh_replica = u32(0x40)
|
||||
pub const refresh_slave = u32(0x40) // same as refresh_replica
|
||||
pub const refresh_master = u32(0x80)
|
||||
pub const refresh_error_log = u32(0x100)
|
||||
pub const refresh_engine_log = u32(0x200)
|
||||
pub const refresh_binary_log = u32(0x400)
|
||||
pub const refresh_relay_log = u32(0x800)
|
||||
pub const refresh_general_log = u32(0x1000)
|
||||
pub const refresh_slow_log = u32(0x2000)
|
||||
pub const refresh_read_lock = u32(0x4000)
|
||||
pub const refresh_fast = u32(0x8000)
|
||||
pub const refresh_user_resources = u32(0x80000)
|
||||
pub const refresh_for_export = u32(0x100000)
|
||||
pub const refresh_optimizer_costs = u32(0x200000)
|
||||
pub const refresh_persist = u32(0x400000)
|
||||
@@ -0,0 +1,94 @@
|
||||
module mysql
|
||||
|
||||
// SslMode mirrors the values of the C `enum mysql_ssl_mode`, used with
|
||||
// `MYSQL_OPT_SSL_MODE`. Use `.unset` to leave the option untouched, in which
|
||||
// case the underlying libmysqlclient default applies (typically `.preferred`).
|
||||
pub enum SslMode {
|
||||
unset = 0
|
||||
disabled = 1
|
||||
preferred = 2
|
||||
required = 3
|
||||
verify_ca = 4
|
||||
verify_identity = 5
|
||||
}
|
||||
|
||||
// FieldType is a list of all supported MYSQL field types.
|
||||
pub enum FieldType {
|
||||
type_decimal
|
||||
type_tiny
|
||||
type_short
|
||||
type_long
|
||||
type_float
|
||||
type_double
|
||||
type_null
|
||||
type_timestamp
|
||||
type_longlong
|
||||
type_int24
|
||||
type_date
|
||||
type_time
|
||||
type_datetime
|
||||
type_year
|
||||
type_newdate
|
||||
type_varchar
|
||||
type_bit
|
||||
type_timestamp2
|
||||
type_datetime2
|
||||
type_time2
|
||||
type_json = 245
|
||||
type_newdecimal
|
||||
type_enum
|
||||
type_set
|
||||
type_tiny_blob
|
||||
type_medium_blob
|
||||
type_long_blob
|
||||
type_blob
|
||||
type_var_string
|
||||
type_string
|
||||
type_geometry
|
||||
}
|
||||
|
||||
// str returns a text representation of the field type `f`.
|
||||
pub fn (f FieldType) str() string {
|
||||
return match f {
|
||||
.type_decimal { 'decimal' }
|
||||
.type_tiny { 'tiny' }
|
||||
.type_short { 'short' }
|
||||
.type_long { 'long' }
|
||||
.type_float { 'float' }
|
||||
.type_double { 'double' }
|
||||
.type_null { 'null' }
|
||||
.type_timestamp { 'timestamp' }
|
||||
.type_longlong { 'longlong' }
|
||||
.type_int24 { 'int24' }
|
||||
.type_date { 'date' }
|
||||
.type_time { 'time' }
|
||||
.type_datetime { 'datetime' }
|
||||
.type_year { 'year' }
|
||||
.type_newdate { 'newdate' }
|
||||
.type_varchar { 'varchar' }
|
||||
.type_bit { 'bit' }
|
||||
.type_timestamp2 { 'timestamp2' }
|
||||
.type_datetime2 { 'datetime2' }
|
||||
.type_time2 { 'time2' }
|
||||
.type_json { 'json' }
|
||||
.type_newdecimal { 'newdecimal' }
|
||||
.type_enum { 'enum' }
|
||||
.type_set { 'set' }
|
||||
.type_tiny_blob { 'tiny_blob' }
|
||||
.type_medium_blob { 'medium_blob' }
|
||||
.type_long_blob { 'long_blob' }
|
||||
.type_blob { 'blob' }
|
||||
.type_var_string { 'var_string' }
|
||||
.type_string { 'string' }
|
||||
.type_geometry { 'geometry' }
|
||||
}
|
||||
}
|
||||
|
||||
// get_len returns the length in bytes, for the given field type `f`.
|
||||
// Should be deleted after the `time` type reimplementation.
|
||||
pub fn (f FieldType) get_len() u32 {
|
||||
return match f {
|
||||
.type_blob { 262140 }
|
||||
else { 0 }
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,12 @@
|
||||
// MYSQL_OPT_SSL_MODE was added in MySQL 5.7. MariaDB's server and connector
|
||||
// headers expose their own TLS options instead, so the generated C code would
|
||||
// fail to compile when it references the MySQL-only enum value. Because enum
|
||||
// values are not preprocessor macros, `#ifndef MYSQL_OPT_SSL_MODE` alone
|
||||
// cannot distinguish a MySQL header that has the enum from a MariaDB header
|
||||
// that does not. Gate on MariaDB's published macros and supply a sentinel that
|
||||
// mysql_options() will reject at runtime if Config.ssl_mode is used there.
|
||||
#if !defined(MYSQL_OPT_SSL_MODE) && \
|
||||
(defined(MARIADB_BASE_VERSION) || defined(MARIADB_VERSION_ID) || \
|
||||
defined(MARIADB_PACKAGE_VERSION_ID) || defined(LIBMARIADB))
|
||||
#define MYSQL_OPT_SSL_MODE 9999
|
||||
#endif
|
||||
@@ -0,0 +1,361 @@
|
||||
// vtest build: started_mysqld?
|
||||
import orm
|
||||
import db.mysql
|
||||
import time
|
||||
|
||||
struct TestCustomSqlType {
|
||||
id int @[primary; sql: serial]
|
||||
custom string @[sql_type: 'TEXT']
|
||||
custom1 string @[sql_type: 'VARCHAR(191)']
|
||||
custom2 string @[sql_type: 'datetime(3)']
|
||||
custom3 string @[sql_type: 'MEDIUMINT']
|
||||
custom4 string @[sql_type: 'DATETIME']
|
||||
custom5 string @[sql_type: 'datetime']
|
||||
}
|
||||
|
||||
struct TestCustomWrongSqlType {
|
||||
id int @[primary; sql: serial]
|
||||
custom string
|
||||
custom1 string @[sql_type: 'VARCHAR']
|
||||
custom2 string @[sql_type: 'money']
|
||||
custom3 string @[sql_type: 'xml']
|
||||
}
|
||||
|
||||
struct TestTimeType {
|
||||
mut:
|
||||
id int @[primary; sql: serial]
|
||||
username string
|
||||
created_at time.Time @[sql_type: 'DATETIME']
|
||||
updated_at string @[sql_type: 'DATETIME']
|
||||
deleted_at time.Time
|
||||
null_date ?time.Time @[sql_type: 'DATETIME']
|
||||
}
|
||||
|
||||
struct TestDefaultAttribute {
|
||||
id string @[primary; sql: serial]
|
||||
name string
|
||||
created_at string @[default: 'CURRENT_TIMESTAMP'; sql_type: 'TIMESTAMP']
|
||||
}
|
||||
|
||||
@[comment: 'This is a table comment']
|
||||
struct TestCommentAttribute {
|
||||
id string @[primary; sql: serial]
|
||||
name string @[comment: 'real user name']
|
||||
created_at string @[default: 'CURRENT_TIMESTAMP'; sql_type: 'TIMESTAMP']
|
||||
}
|
||||
|
||||
fn test_mysql_orm() {
|
||||
$if !network ? {
|
||||
eprintln('> Skipping test ${@FN}, since `-d network` is not passed.')
|
||||
eprintln('> This test requires a working mysql server running on localhost.')
|
||||
return
|
||||
}
|
||||
mut db := mysql.connect(
|
||||
host: '127.0.0.1'
|
||||
port: 3306
|
||||
username: 'root'
|
||||
password: '12345678'
|
||||
dbname: 'mysql'
|
||||
)!
|
||||
defer {
|
||||
db.close() or {}
|
||||
}
|
||||
table := orm.Table{
|
||||
name: 'Test'
|
||||
}
|
||||
db.drop(table) or {}
|
||||
db.create(table, [
|
||||
orm.TableField{
|
||||
name: 'id'
|
||||
typ: typeof[int]().idx
|
||||
attrs: [
|
||||
VAttribute{
|
||||
name: 'primary'
|
||||
},
|
||||
VAttribute{
|
||||
name: 'sql'
|
||||
has_arg: true
|
||||
kind: .plain
|
||||
arg: 'serial'
|
||||
},
|
||||
]
|
||||
},
|
||||
orm.TableField{
|
||||
name: 'name'
|
||||
typ: typeof[string]().idx
|
||||
attrs: []
|
||||
},
|
||||
orm.TableField{
|
||||
name: 'age'
|
||||
typ: typeof[int]().idx
|
||||
},
|
||||
]) or { panic(err) }
|
||||
|
||||
db.insert(table, orm.QueryData{
|
||||
fields: ['name', 'age']
|
||||
data: [orm.string_to_primitive('Louis'), orm.int_to_primitive(101)]
|
||||
}) or { panic(err) }
|
||||
|
||||
res := db.select(orm.SelectConfig{
|
||||
table: table
|
||||
has_where: true
|
||||
fields: ['id', 'name', 'age']
|
||||
types: [typeof[int]().idx, typeof[string]().idx, typeof[i64]().idx]
|
||||
}, orm.QueryData{}, orm.QueryData{
|
||||
fields: ['name', 'age']
|
||||
data: [orm.Primitive('Louis'), i64(101)]
|
||||
types: [typeof[string]().idx, typeof[i64]().idx]
|
||||
is_and: [true, true]
|
||||
kinds: [.eq, .eq]
|
||||
}) or { panic(err) }
|
||||
|
||||
id := res[0][0]
|
||||
name := res[0][1]
|
||||
age := res[0][2]
|
||||
|
||||
assert id is int
|
||||
if id is int {
|
||||
assert id == 1
|
||||
}
|
||||
|
||||
assert name is string
|
||||
if name is string {
|
||||
assert name == 'Louis'
|
||||
}
|
||||
|
||||
assert age is i64
|
||||
if age is i64 {
|
||||
assert age == 101
|
||||
}
|
||||
|
||||
sum_res := db.select(orm.SelectConfig{
|
||||
table: table
|
||||
aggregate_kind: .sum
|
||||
aggregate_field: 'age'
|
||||
fields: ['age']
|
||||
types: [typeof[int]().idx]
|
||||
}, orm.QueryData{}, orm.QueryData{}) or { panic(err) }
|
||||
assert sum_res.len == 1
|
||||
assert sum_res[0].len == 1
|
||||
sum_val := sum_res[0][0]
|
||||
assert sum_val is int
|
||||
if sum_val is int {
|
||||
assert sum_val == 101
|
||||
}
|
||||
|
||||
avg_res := db.select(orm.SelectConfig{
|
||||
table: table
|
||||
aggregate_kind: .avg
|
||||
aggregate_field: 'age'
|
||||
fields: ['age']
|
||||
types: [typeof[f64]().idx]
|
||||
}, orm.QueryData{}, orm.QueryData{}) or { panic(err) }
|
||||
assert avg_res.len == 1
|
||||
assert avg_res[0].len == 1
|
||||
avg_val := avg_res[0][0]
|
||||
assert avg_val is f64
|
||||
if avg_val is f64 {
|
||||
assert avg_val == 101.0
|
||||
}
|
||||
|
||||
/** test orm sql type
|
||||
* - verify if all type create by attribute sql_type has created
|
||||
*/
|
||||
sql db {
|
||||
create table TestCustomSqlType
|
||||
}!
|
||||
|
||||
mut result_custom_sql := db.query("
|
||||
SELECT DATA_TYPE, COLUMN_TYPE
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_NAME = 'testcustomsqltype'
|
||||
ORDER BY ORDINAL_POSITION
|
||||
") or {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
information_schema_custom_sql := [
|
||||
{
|
||||
'DATA_TYPE': 'bigint'
|
||||
'COLUMN_TYPE': 'bigint unsigned'
|
||||
},
|
||||
{
|
||||
'DATA_TYPE': 'text'
|
||||
'COLUMN_TYPE': 'text'
|
||||
},
|
||||
{
|
||||
'DATA_TYPE': 'varchar'
|
||||
'COLUMN_TYPE': 'varchar(191)'
|
||||
},
|
||||
{
|
||||
'DATA_TYPE': 'datetime'
|
||||
'COLUMN_TYPE': 'datetime(3)'
|
||||
},
|
||||
{
|
||||
'DATA_TYPE': 'mediumint'
|
||||
'COLUMN_TYPE': 'mediumint'
|
||||
},
|
||||
{
|
||||
'DATA_TYPE': 'datetime'
|
||||
'COLUMN_TYPE': 'datetime'
|
||||
},
|
||||
{
|
||||
'DATA_TYPE': 'datetime'
|
||||
'COLUMN_TYPE': 'datetime'
|
||||
},
|
||||
]
|
||||
|
||||
sql db {
|
||||
drop table TestCustomSqlType
|
||||
}!
|
||||
|
||||
assert result_custom_sql.maps() == information_schema_custom_sql
|
||||
|
||||
/** test_orm_time_type
|
||||
* - test time.Time v type with sql_type: 'TIMESTAMP'
|
||||
* - test string v type with sql_type: 'TIMESTAMP'
|
||||
* - test time.Time v type without
|
||||
*/
|
||||
today := time.parse('2022-07-16 15:13:27') or {
|
||||
println(err)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
model1 := TestTimeType{
|
||||
username: 'hitalo'
|
||||
created_at: today
|
||||
updated_at: today.str()
|
||||
deleted_at: today
|
||||
// null_date is null
|
||||
}
|
||||
model2 := TestTimeType{
|
||||
username: 'tom'
|
||||
created_at: today
|
||||
updated_at: today.str()
|
||||
deleted_at: today
|
||||
null_date: today
|
||||
}
|
||||
model3 := TestTimeType{
|
||||
username: 'kitty'
|
||||
created_at: today
|
||||
updated_at: today.str()
|
||||
deleted_at: today
|
||||
// null_date is null
|
||||
}
|
||||
|
||||
sql db {
|
||||
create table TestTimeType
|
||||
}!
|
||||
|
||||
sql db {
|
||||
insert model1 into TestTimeType
|
||||
insert model2 into TestTimeType
|
||||
insert model3 into TestTimeType
|
||||
}!
|
||||
|
||||
results := sql db {
|
||||
select from TestTimeType
|
||||
}!
|
||||
|
||||
sql db {
|
||||
drop table TestTimeType
|
||||
}!
|
||||
|
||||
assert results[0].created_at == model1.created_at
|
||||
assert results[0].username == model1.username
|
||||
assert results[0].updated_at == model1.updated_at
|
||||
assert results[0].deleted_at == model1.deleted_at
|
||||
assert results[0].null_date == none
|
||||
|
||||
assert results[1].created_at == model2.created_at
|
||||
assert results[1].username == model2.username
|
||||
assert results[1].updated_at == model2.updated_at
|
||||
assert results[1].deleted_at == model2.deleted_at
|
||||
if x := results[1].null_date {
|
||||
// should not by `none`/`NULL`
|
||||
assert x == model2.deleted_at
|
||||
} else {
|
||||
assert false
|
||||
}
|
||||
|
||||
assert results[2].created_at == model3.created_at
|
||||
assert results[2].username == model3.username
|
||||
assert results[2].updated_at == model3.updated_at
|
||||
assert results[2].deleted_at == model3.deleted_at
|
||||
assert results[2].null_date == none
|
||||
|
||||
/** test default attribute
|
||||
*/
|
||||
sql db {
|
||||
create table TestDefaultAttribute
|
||||
}!
|
||||
|
||||
mut result_defaults := db.query("
|
||||
SELECT COLUMN_DEFAULT
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_NAME = 'testdefaultattribute'
|
||||
ORDER BY ORDINAL_POSITION
|
||||
") or {
|
||||
println(err)
|
||||
panic(err)
|
||||
}
|
||||
mut information_schema_defaults_results := []string{}
|
||||
|
||||
sql db {
|
||||
drop table TestDefaultAttribute
|
||||
}!
|
||||
|
||||
information_schema_column_default_sql := [{
|
||||
'COLUMN_DEFAULT': ''
|
||||
}, {
|
||||
'COLUMN_DEFAULT': ''
|
||||
}, {
|
||||
'COLUMN_DEFAULT': 'CURRENT_TIMESTAMP'
|
||||
}]
|
||||
assert information_schema_column_default_sql == result_defaults.maps()
|
||||
|
||||
/** test comment attribute
|
||||
*/
|
||||
sql db {
|
||||
create table TestCommentAttribute
|
||||
}!
|
||||
|
||||
mut column_comments := db.query("
|
||||
SELECT COLUMN_COMMENT
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_NAME = 'testcommentattribute'
|
||||
ORDER BY ORDINAL_POSITION
|
||||
") or {
|
||||
println(err)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
mut table_comment := db.query("
|
||||
SELECT TABLE_COMMENT
|
||||
FROM INFORMATION_SCHEMA.TABLES
|
||||
WHERE TABLE_NAME = 'testcommentattribute'
|
||||
") or {
|
||||
println(err)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
sql db {
|
||||
drop table TestCommentAttribute
|
||||
}!
|
||||
|
||||
information_schema_column_comment_sql := [{
|
||||
'COLUMN_COMMENT': ''
|
||||
}, {
|
||||
'COLUMN_COMMENT': 'real user name'
|
||||
}, {
|
||||
'COLUMN_COMMENT': ''
|
||||
}]
|
||||
assert information_schema_column_comment_sql == column_comments.maps()
|
||||
|
||||
information_schema_table_comment_sql := [
|
||||
{
|
||||
'TABLE_COMMENT': 'This is a table comment'
|
||||
},
|
||||
]
|
||||
assert information_schema_table_comment_sql == table_comment.maps()
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
// vtest build: started_mysqld?
|
||||
import db.mysql
|
||||
import orm
|
||||
|
||||
fn test_mysql() {
|
||||
$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: '12345678'
|
||||
dbname: 'mysql'
|
||||
}
|
||||
|
||||
mut db := mysql.connect(config)!
|
||||
defer {
|
||||
db.close() or {}
|
||||
}
|
||||
|
||||
assert db.validate()!
|
||||
|
||||
mut conn := orm.TransactionalConnection(db)
|
||||
mut tx := orm.begin(mut conn)!
|
||||
tx.transaction[int](fn (mut tx orm.Tx) !int {
|
||||
return 1
|
||||
})!
|
||||
tx.commit()!
|
||||
|
||||
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', '']
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn test_mysql_multi_statements() {
|
||||
$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: '12345678'
|
||||
dbname: 'mysql'
|
||||
flag: mysql.ConnectionFlag.client_multi_statements
|
||||
}
|
||||
|
||||
mut db := mysql.connect(config)!
|
||||
defer {
|
||||
db.close() or {}
|
||||
}
|
||||
|
||||
// exec_multi drains every result set so the connection stays usable.
|
||||
results := db.exec_multi('SELECT 1 AS a; SELECT 2 AS b; SELECT 3 AS c')!
|
||||
assert results.len == 3
|
||||
assert results[0][0].val(0) == '1'
|
||||
assert results[1][0].val(0) == '2'
|
||||
assert results[2][0].val(0) == '3'
|
||||
|
||||
// The connection must remain reusable afterwards: regression test for #18061.
|
||||
follow_up := db.query('SELECT 42')!
|
||||
rows := follow_up.rows()
|
||||
assert rows[0].val(0) == '42'
|
||||
|
||||
// Low-level drain via more_results/next_result/store_result also works.
|
||||
first := db.query('SELECT 10; SELECT 20')!
|
||||
assert first.rows()[0].val(0) == '10'
|
||||
unsafe { first.free() }
|
||||
assert db.more_results() == true
|
||||
assert db.next_result()! == true
|
||||
second := db.store_result()!
|
||||
assert second.rows()[0].val(0) == '20'
|
||||
unsafe { second.free() }
|
||||
assert db.next_result()! == false
|
||||
// After draining, new queries succeed (no "Commands out of sync" error).
|
||||
final := db.query('SELECT 99')!
|
||||
assert final.rows()[0].val(0) == '99'
|
||||
|
||||
// Regression test for #18063: every statement in a multi-statement query
|
||||
// must complete on the server before exec_multi() returns, so that the
|
||||
// next query can immediately rely on its side effects (e.g. tables).
|
||||
_ := db.exec_multi('DROP TABLE IF EXISTS multi_a;
|
||||
DROP TABLE IF EXISTS multi_b;
|
||||
CREATE TABLE multi_a (id INT PRIMARY KEY);
|
||||
CREATE TABLE multi_b (id INT PRIMARY KEY);
|
||||
INSERT INTO multi_a (id) VALUES (1), (2);
|
||||
INSERT INTO multi_b (id) VALUES (10), (20), (30);')!
|
||||
count_a := db.query('SELECT COUNT(*) FROM multi_a')!.rows()
|
||||
count_b := db.query('SELECT COUNT(*) FROM multi_b')!.rows()
|
||||
assert count_a[0].val(0) == '2'
|
||||
assert count_b[0].val(0) == '3'
|
||||
db.exec_none('DROP TABLE IF EXISTS multi_a')
|
||||
db.exec_none('DROP TABLE IF EXISTS multi_b')
|
||||
}
|
||||
|
||||
fn mysql_query_count_from_shared_connection(db mysql.DB) !int {
|
||||
result := db.query('SELECT COUNT(*) as table_count FROM information_schema.tables')!
|
||||
rows := result.maps()
|
||||
return rows[0]['table_count'].int()
|
||||
}
|
||||
|
||||
fn test_query_is_serialized_for_shared_connections() {
|
||||
$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: '12345678'
|
||||
dbname: 'mysql'
|
||||
}
|
||||
|
||||
mut db := mysql.connect(config)!
|
||||
defer {
|
||||
db.close() or {}
|
||||
}
|
||||
|
||||
threads := [
|
||||
spawn mysql_query_count_from_shared_connection(db),
|
||||
spawn mysql_query_count_from_shared_connection(db),
|
||||
spawn mysql_query_count_from_shared_connection(db),
|
||||
spawn mysql_query_count_from_shared_connection(db),
|
||||
]
|
||||
results := threads.wait()!
|
||||
assert results.len == 4
|
||||
for count in results {
|
||||
assert count > 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,550 @@
|
||||
module mysql
|
||||
|
||||
import orm
|
||||
import strconv
|
||||
import time
|
||||
|
||||
// select is used internally by V's ORM for processing `SELECT ` queries.
|
||||
pub fn (db DB) select(config orm.SelectConfig, data orm.QueryData, where orm.QueryData) ![][]orm.Primitive {
|
||||
where_with_tenant := orm.apply_tenant_filter(config.table, where)
|
||||
query := orm.orm_select_gen(config, '`', false, '?', 0, where_with_tenant)
|
||||
mut result := [][]orm.Primitive{}
|
||||
mut stmt := db.init_stmt(query)
|
||||
stmt.prepare()!
|
||||
|
||||
mysql_stmt_bind_query_data(mut stmt, where_with_tenant)!
|
||||
mysql_stmt_bind_query_data(mut stmt, data)!
|
||||
|
||||
if data.data.len > 0 || where_with_tenant.data.len > 0 {
|
||||
stmt.bind_params()!
|
||||
}
|
||||
|
||||
stmt.execute()!
|
||||
metadata := stmt.gen_metadata()
|
||||
fields := stmt.fetch_fields(metadata)
|
||||
num_fields := stmt.get_field_count()
|
||||
mut data_pointers := []&u8{cap: int(num_fields)}
|
||||
|
||||
// Allocate memory for each column.
|
||||
for i in 0 .. num_fields {
|
||||
field := unsafe { fields[i] }
|
||||
match unsafe { FieldType(field.type) } {
|
||||
.type_tiny {
|
||||
data_pointers << unsafe { malloc(1) }
|
||||
}
|
||||
.type_short {
|
||||
data_pointers << unsafe { malloc(2) }
|
||||
}
|
||||
.type_long, .type_float {
|
||||
data_pointers << unsafe { malloc(4) }
|
||||
}
|
||||
.type_longlong, .type_double {
|
||||
data_pointers << unsafe { malloc(8) }
|
||||
}
|
||||
.type_time, .type_date, .type_datetime, .type_time2, .type_datetime2, .type_timestamp {
|
||||
data_pointers << unsafe { malloc(sizeof(C.MYSQL_TIME)) }
|
||||
}
|
||||
.type_decimal, .type_newdecimal, .type_string, .type_var_string, .type_blob,
|
||||
.type_tiny_blob, .type_medium_blob, .type_long_blob {
|
||||
// Memory will be allocated later dynamically depending on the length of the value.
|
||||
data_pointers << &u8(unsafe { nil })
|
||||
}
|
||||
else {
|
||||
return error('\'${unsafe { FieldType(field.type) }}\' is not yet implemented. Please create a new issue at https://github.com/vlang/v/issues/new')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mut lengths := []u32{len: int(num_fields), init: 0}
|
||||
mut is_null := []bool{len: int(num_fields)}
|
||||
stmt.bind_res(fields, data_pointers, lengths, is_null, num_fields)
|
||||
|
||||
mut types := config.types.clone()
|
||||
mut field_types := []FieldType{}
|
||||
if config.aggregate_kind == .count {
|
||||
types = [orm.type_idx['u64']]
|
||||
}
|
||||
|
||||
// Map stores column indexes and their binds in order to extract values
|
||||
// for these columns separately, with individual memory allocation for each value.
|
||||
mut string_binds_map := map[int]C.MYSQL_BIND{}
|
||||
|
||||
for i, mut mysql_bind in stmt.res {
|
||||
field := unsafe { fields[i] }
|
||||
field_type := unsafe { FieldType(field.type) }
|
||||
field_types << field_type
|
||||
|
||||
match field_type {
|
||||
.type_decimal, .type_newdecimal, .type_string, .type_var_string, .type_blob,
|
||||
.type_tiny_blob, .type_medium_blob, .type_long_blob {
|
||||
if field_type in [.type_decimal, .type_newdecimal] {
|
||||
mysql_bind.buffer_type = C.MYSQL_TYPE_STRING
|
||||
}
|
||||
string_binds_map[i] = mysql_bind
|
||||
}
|
||||
.type_long {
|
||||
mysql_bind.buffer_type = C.MYSQL_TYPE_LONG
|
||||
}
|
||||
.type_time, .type_date, .type_datetime, .type_timestamp {
|
||||
// FIXME: Allocate memory for blobs dynamically.
|
||||
mysql_bind.buffer_type = C.MYSQL_TYPE_BLOB
|
||||
mysql_bind.buffer_length = FieldType.type_blob.get_len()
|
||||
}
|
||||
else {}
|
||||
}
|
||||
}
|
||||
|
||||
stmt.bind_result_buffer()!
|
||||
stmt.store_result()!
|
||||
|
||||
for {
|
||||
// Fetch every row from the `select` result.
|
||||
status := stmt.fetch_stmt()!
|
||||
is_error := status == 1
|
||||
are_no_rows_to_fetch := status == mysql_no_data
|
||||
|
||||
if is_error || are_no_rows_to_fetch {
|
||||
break
|
||||
}
|
||||
|
||||
// Fetch columns that should be allocated dynamically.
|
||||
for index, mut bind in string_binds_map {
|
||||
string_length := lengths[index] + 1
|
||||
data_pointers[index] = unsafe { malloc(string_length) }
|
||||
bind.buffer = data_pointers[index]
|
||||
bind.buffer_length = string_length
|
||||
bind.length = unsafe { nil }
|
||||
|
||||
stmt.fetch_column(bind, index)!
|
||||
}
|
||||
|
||||
mut row := data_pointers_to_primitives(is_null, data_pointers, types, field_types)!
|
||||
if config.aggregate_kind == .count && row.len > 0 {
|
||||
count_value := row[0]
|
||||
row[0] = match count_value {
|
||||
u64 { orm.Primitive(int(count_value)) }
|
||||
i64 { orm.Primitive(int(count_value)) }
|
||||
int { count_value }
|
||||
else { count_value }
|
||||
}
|
||||
}
|
||||
result << row
|
||||
}
|
||||
|
||||
stmt.close()!
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// insert is used internally by V's ORM for processing `INSERT ` queries
|
||||
pub fn (db DB) insert(table orm.Table, data orm.QueryData) ! {
|
||||
mut converted_primitive_array := db.convert_query_data_to_primitives(table.name, data)!
|
||||
|
||||
converted_primitive_data := orm.QueryData{
|
||||
fields: data.fields
|
||||
data: converted_primitive_array
|
||||
types: data.types
|
||||
parentheses: data.parentheses
|
||||
kinds: data.kinds
|
||||
auto_fields: data.auto_fields
|
||||
is_and: data.is_and
|
||||
batch_rows: data.batch_rows
|
||||
batch_key: data.batch_key
|
||||
}
|
||||
|
||||
query, converted_data := orm.orm_stmt_gen(.default, table, '`', .insert, false, '?', 1,
|
||||
converted_primitive_data, orm.QueryData{})
|
||||
mysql_stmt_worker(db, query, converted_data, orm.QueryData{})!
|
||||
}
|
||||
|
||||
// update is used internally by V's ORM for processing `UPDATE ` queries
|
||||
pub fn (db DB) update(table orm.Table, data orm.QueryData, where orm.QueryData) ! {
|
||||
where_with_tenant := orm.apply_tenant_filter(table, where)
|
||||
query, _ := orm.orm_stmt_gen(.default, table, '`', .update, false, '?', 1, data,
|
||||
where_with_tenant)
|
||||
mysql_stmt_worker(db, query, data, where_with_tenant)!
|
||||
}
|
||||
|
||||
// delete is used internally by V's ORM for processing `DELETE ` queries
|
||||
pub fn (db DB) delete(table orm.Table, where orm.QueryData) ! {
|
||||
where_with_tenant := orm.apply_tenant_filter(table, where)
|
||||
query, _ := orm.orm_stmt_gen(.default, table, '`', .delete, false, '?', 1, orm.QueryData{},
|
||||
where_with_tenant)
|
||||
mysql_stmt_worker(db, query, orm.QueryData{}, where_with_tenant)!
|
||||
}
|
||||
|
||||
// last_id is used internally by V's ORM for post-processing `INSERT ` queries
|
||||
pub fn (db DB) last_id() int {
|
||||
query := 'SELECT last_insert_id();'
|
||||
id := db.query(query) or { return 0 }
|
||||
|
||||
return id.rows()[0].vals[0].int()
|
||||
}
|
||||
|
||||
// create is used internally by V's ORM for processing table creation queries (DDL)
|
||||
pub fn (db DB) create(table orm.Table, fields []orm.TableField) ! {
|
||||
query := orm.orm_table_gen(.mysql, table, '`', true, 0, fields, mysql_type_from_v, false) or {
|
||||
return err
|
||||
}
|
||||
mysql_stmt_worker(db, query, orm.QueryData{}, orm.QueryData{})!
|
||||
}
|
||||
|
||||
// drop is used internally by V's ORM for processing table destroying queries (DDL)
|
||||
pub fn (db DB) drop(table orm.Table) ! {
|
||||
query := 'DROP TABLE `${table.name}`;'
|
||||
mysql_stmt_worker(db, query, orm.QueryData{}, orm.QueryData{})!
|
||||
}
|
||||
|
||||
// orm_begin starts a transaction for ORM helpers.
|
||||
pub fn (mut db DB) orm_begin() ! {
|
||||
db.begin()!
|
||||
}
|
||||
|
||||
// orm_commit commits a transaction for ORM helpers.
|
||||
pub fn (mut db DB) orm_commit() ! {
|
||||
db.commit()!
|
||||
}
|
||||
|
||||
// orm_rollback rolls back a transaction for ORM helpers.
|
||||
pub fn (mut db DB) orm_rollback() ! {
|
||||
db.rollback()!
|
||||
}
|
||||
|
||||
// orm_savepoint creates a savepoint for ORM helpers.
|
||||
pub fn (mut db DB) orm_savepoint(name string) ! {
|
||||
db.savepoint(name)!
|
||||
}
|
||||
|
||||
// orm_rollback_to rolls back to a savepoint for ORM helpers.
|
||||
pub fn (mut db DB) orm_rollback_to(name string) ! {
|
||||
db.rollback_to(name)!
|
||||
}
|
||||
|
||||
// orm_release_savepoint releases a savepoint for ORM helpers.
|
||||
pub fn (mut db DB) orm_release_savepoint(name string) ! {
|
||||
db.release_savepoint(name)!
|
||||
}
|
||||
|
||||
// mysql_stmt_worker executes the `query` with the provided `data` and `where` parameters
|
||||
// without returning the result.
|
||||
// This is commonly used for `INSERT`, `UPDATE`, `CREATE`, `DROP`, and `DELETE` queries.
|
||||
fn mysql_stmt_worker(db DB, query string, data orm.QueryData, where orm.QueryData) ! {
|
||||
mut stmt := db.init_stmt(query)
|
||||
stmt.prepare()!
|
||||
|
||||
mysql_stmt_bind_query_data(mut stmt, data)!
|
||||
mysql_stmt_bind_query_data(mut stmt, where)!
|
||||
|
||||
if data.data.len > 0 || where.data.len > 0 {
|
||||
stmt.bind_params()!
|
||||
}
|
||||
|
||||
stmt.execute()!
|
||||
stmt.close()!
|
||||
}
|
||||
|
||||
// mysql_stmt_bind_query_data binds all the fields of `q` to the `stmt`.
|
||||
fn mysql_stmt_bind_query_data(mut stmt Stmt, d orm.QueryData) ! {
|
||||
for data in d.data {
|
||||
stmt_bind_primitive(mut stmt, data)
|
||||
}
|
||||
}
|
||||
|
||||
fn stmt_bind_array[T](mut stmt Stmt, data []T) {
|
||||
for element in data {
|
||||
stmt_bind_primitive(mut stmt, orm.Primitive(element))
|
||||
}
|
||||
}
|
||||
|
||||
// stmt_bind_primitive binds the `data` to the `stmt`.
|
||||
fn stmt_bind_primitive(mut stmt Stmt, data orm.Primitive) {
|
||||
match data {
|
||||
bool {
|
||||
stmt.bind_bool(&data)
|
||||
}
|
||||
i8 {
|
||||
stmt.bind_i8(&data)
|
||||
}
|
||||
i16 {
|
||||
stmt.bind_i16(&data)
|
||||
}
|
||||
int {
|
||||
stmt.bind_int(&data)
|
||||
}
|
||||
i64 {
|
||||
stmt.bind_i64(&data)
|
||||
}
|
||||
u8 {
|
||||
stmt.bind_u8(&data)
|
||||
}
|
||||
u16 {
|
||||
stmt.bind_u16(&data)
|
||||
}
|
||||
u32 {
|
||||
stmt.bind_u32(&data)
|
||||
}
|
||||
u64 {
|
||||
stmt.bind_u64(&data)
|
||||
}
|
||||
f32 {
|
||||
stmt.bind_f32(unsafe { &f32(&data) })
|
||||
}
|
||||
f64 {
|
||||
stmt.bind_f64(unsafe { &f64(&data) })
|
||||
}
|
||||
string {
|
||||
stmt.bind_text(data)
|
||||
}
|
||||
time.Time {
|
||||
unix := int(data.unix())
|
||||
stmt_bind_primitive(mut stmt, unix)
|
||||
}
|
||||
orm.InfixType {
|
||||
stmt_bind_primitive(mut stmt, data.right)
|
||||
}
|
||||
orm.Null {
|
||||
stmt.bind_null()
|
||||
}
|
||||
[]orm.Primitive {
|
||||
stmt_bind_array(mut stmt, data)
|
||||
}
|
||||
[]bool {
|
||||
stmt_bind_array(mut stmt, data)
|
||||
}
|
||||
[]f32 {
|
||||
stmt_bind_array(mut stmt, data)
|
||||
}
|
||||
[]f64 {
|
||||
stmt_bind_array(mut stmt, data)
|
||||
}
|
||||
[]i16 {
|
||||
stmt_bind_array(mut stmt, data)
|
||||
}
|
||||
[]i64 {
|
||||
stmt_bind_array(mut stmt, data)
|
||||
}
|
||||
[]i8 {
|
||||
stmt_bind_array(mut stmt, data)
|
||||
}
|
||||
[]int {
|
||||
stmt_bind_array(mut stmt, data)
|
||||
}
|
||||
[]string {
|
||||
stmt_bind_array(mut stmt, data)
|
||||
}
|
||||
[]time.Time {
|
||||
stmt_bind_array(mut stmt, data)
|
||||
}
|
||||
[]u16 {
|
||||
stmt_bind_array(mut stmt, data)
|
||||
}
|
||||
[]u32 {
|
||||
stmt_bind_array(mut stmt, data)
|
||||
}
|
||||
[]u64 {
|
||||
stmt_bind_array(mut stmt, data)
|
||||
}
|
||||
[]u8 {
|
||||
stmt_bind_array(mut stmt, data)
|
||||
}
|
||||
[]orm.InfixType {
|
||||
stmt_bind_array(mut stmt, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// data_pointers_to_primitives returns an array of `Primitive`
|
||||
// cast from `data_pointers` using `types`.
|
||||
fn data_pointers_to_primitives(is_null []bool, data_pointers []&u8, types []int, field_types []FieldType) ![]orm.Primitive {
|
||||
mut result := []orm.Primitive{}
|
||||
|
||||
for i, data in data_pointers {
|
||||
mut primitive := orm.Primitive(0)
|
||||
if !is_null[i] {
|
||||
if field_types[i] in [.type_decimal, .type_newdecimal] {
|
||||
decimal_value := unsafe { cstring_to_vstring(&char(data)) }
|
||||
primitive = decimal_string_to_primitive(decimal_value, types[i])!
|
||||
result << primitive
|
||||
continue
|
||||
}
|
||||
match types[i] {
|
||||
orm.type_idx['i8'] {
|
||||
primitive = *(unsafe { &i8(data) })
|
||||
}
|
||||
orm.type_idx['i16'] {
|
||||
primitive = *(unsafe { &i16(data) })
|
||||
}
|
||||
orm.type_idx['int'], orm.serial {
|
||||
primitive = *(unsafe { &int(data) })
|
||||
}
|
||||
orm.type_idx['i64'] {
|
||||
primitive = *(unsafe { &i64(data) })
|
||||
}
|
||||
orm.type_idx['u8'] {
|
||||
primitive = *(unsafe { &u8(data) })
|
||||
}
|
||||
orm.type_idx['u16'] {
|
||||
primitive = *(unsafe { &u16(data) })
|
||||
}
|
||||
orm.type_idx['u32'] {
|
||||
primitive = *(unsafe { &u32(data) })
|
||||
}
|
||||
orm.type_idx['u64'] {
|
||||
primitive = *(unsafe { &u64(data) })
|
||||
}
|
||||
orm.type_idx['f32'] {
|
||||
primitive = *(unsafe { &f32(data) })
|
||||
}
|
||||
orm.type_idx['f64'] {
|
||||
primitive = *(unsafe { &f64(data) })
|
||||
}
|
||||
orm.type_idx['bool'] {
|
||||
primitive = *(unsafe { &bool(data) })
|
||||
}
|
||||
orm.type_string {
|
||||
primitive = unsafe { cstring_to_vstring(&char(data)) }
|
||||
}
|
||||
orm.time_ {
|
||||
match field_types[i] {
|
||||
.type_long {
|
||||
timestamp := *(unsafe { &int(data) })
|
||||
primitive = time.unix(timestamp)
|
||||
}
|
||||
.type_datetime, .type_timestamp {
|
||||
primitive = time.parse(unsafe { cstring_to_vstring(&char(data)) })!
|
||||
}
|
||||
else {}
|
||||
}
|
||||
}
|
||||
orm.enum_ {
|
||||
primitive = *(unsafe { &i64(data) })
|
||||
}
|
||||
else {
|
||||
return error('Unknown type ${types[i]}')
|
||||
}
|
||||
}
|
||||
} else {
|
||||
primitive = orm.Null{}
|
||||
}
|
||||
result << primitive
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
fn decimal_string_to_primitive(value string, typ int) !orm.Primitive {
|
||||
return match typ {
|
||||
orm.type_idx['i8'] {
|
||||
orm.Primitive(strconv.atoi8(value)!)
|
||||
}
|
||||
orm.type_idx['i16'] {
|
||||
orm.Primitive(strconv.atoi16(value)!)
|
||||
}
|
||||
orm.type_idx['int'], orm.serial {
|
||||
orm.Primitive(strconv.atoi(value)!)
|
||||
}
|
||||
orm.type_idx['i64'], orm.enum_ {
|
||||
orm.Primitive(strconv.atoi64(value)!)
|
||||
}
|
||||
orm.type_idx['u8'] {
|
||||
orm.Primitive(strconv.atou8(value)!)
|
||||
}
|
||||
orm.type_idx['u16'] {
|
||||
orm.Primitive(strconv.atou16(value)!)
|
||||
}
|
||||
orm.type_idx['u32'] {
|
||||
orm.Primitive(strconv.atou32(value)!)
|
||||
}
|
||||
orm.type_idx['u64'] {
|
||||
orm.Primitive(strconv.atou64(value)!)
|
||||
}
|
||||
orm.type_idx['f32'] {
|
||||
orm.Primitive(f32(strconv.atof64(value)!))
|
||||
}
|
||||
orm.type_idx['f64'] {
|
||||
orm.Primitive(strconv.atof64(value)!)
|
||||
}
|
||||
else {
|
||||
return error('Unknown decimal target type ${typ}')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mysql_type_from_v converts the V type to the corresponding MySQL type.
|
||||
fn mysql_type_from_v(typ int) !string {
|
||||
sql_type := match typ {
|
||||
orm.type_idx['i8'], orm.type_idx['u8'] {
|
||||
'TINYINT'
|
||||
}
|
||||
orm.type_idx['i16'], orm.type_idx['u16'] {
|
||||
'SMALLINT'
|
||||
}
|
||||
orm.type_idx['int'], orm.type_idx['u32'], orm.time_ {
|
||||
'INT'
|
||||
}
|
||||
orm.type_idx['i64'], orm.type_idx['u64'], orm.enum_ {
|
||||
'BIGINT'
|
||||
}
|
||||
orm.type_idx['f32'] {
|
||||
'FLOAT'
|
||||
}
|
||||
orm.type_idx['f64'] {
|
||||
'DOUBLE'
|
||||
}
|
||||
orm.type_string {
|
||||
'TEXT'
|
||||
}
|
||||
orm.serial {
|
||||
'SERIAL'
|
||||
}
|
||||
orm.type_idx['bool'] {
|
||||
'BOOLEAN'
|
||||
}
|
||||
else {
|
||||
return error('Unknown type ${typ}')
|
||||
}
|
||||
}
|
||||
|
||||
return sql_type
|
||||
}
|
||||
|
||||
// convert_query_data_to_primitives converts the `data` representing the `QueryData`
|
||||
// into an array of `Primitive`.
|
||||
fn (db DB) convert_query_data_to_primitives(table string, data orm.QueryData) ![]orm.Primitive {
|
||||
mut column_type_map := db.get_table_column_type_map(table)!
|
||||
mut converted_data := []orm.Primitive{}
|
||||
if data.fields.len == 0 {
|
||||
return converted_data
|
||||
}
|
||||
|
||||
for i, primitive in data.data {
|
||||
field := data.fields[i % data.fields.len]
|
||||
if primitive.type_name() == 'time.Time' {
|
||||
if column_type_map[field] in ['datetime', 'timestamp'] {
|
||||
converted_data << orm.Primitive((primitive as time.Time).str())
|
||||
} else {
|
||||
converted_data << primitive
|
||||
}
|
||||
} else {
|
||||
converted_data << primitive
|
||||
}
|
||||
}
|
||||
|
||||
return converted_data
|
||||
}
|
||||
|
||||
// get_table_column_type_map returns a map where the key represents the column name,
|
||||
// and the value represents its data type.
|
||||
fn (db DB) get_table_column_type_map(table string) !map[string]string {
|
||||
data_type_query := "SELECT COLUMN_NAME, DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '${table}'"
|
||||
mut column_type_map := map[string]string{}
|
||||
results := db.query(data_type_query)!
|
||||
|
||||
for row in results.rows() {
|
||||
column_type_map[row.vals[0]] = row.vals[1]
|
||||
}
|
||||
|
||||
unsafe { results.free() }
|
||||
|
||||
return column_type_map
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
module mysql
|
||||
|
||||
pub struct ConnectionPool {
|
||||
mut:
|
||||
connections chan DB
|
||||
config Config
|
||||
}
|
||||
|
||||
// new_connection_pool creates a new connection pool with the given size and configuration.
|
||||
pub fn new_connection_pool(config Config, size int) !ConnectionPool {
|
||||
mut connections := chan DB{cap: size}
|
||||
for _ in 0 .. size {
|
||||
conn := connect(config)!
|
||||
connections <- conn
|
||||
}
|
||||
return ConnectionPool{
|
||||
connections: connections
|
||||
config: config
|
||||
}
|
||||
}
|
||||
|
||||
// acquire gets a connection from the pool
|
||||
pub fn (mut pool ConnectionPool) acquire() !DB {
|
||||
return <-pool.connections or { return error('Failed to acquire a connection from the pool') }
|
||||
}
|
||||
|
||||
// release returns a connection back to the pool.
|
||||
pub fn (mut pool ConnectionPool) release(conn DB) {
|
||||
pool.connections <- conn
|
||||
}
|
||||
|
||||
// close closes all connections in the pool.
|
||||
pub fn (mut pool ConnectionPool) close() {
|
||||
for _ in 0 .. pool.connections.len {
|
||||
mut conn := <-pool.connections or { break }
|
||||
conn.close() or { break }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
// vtest build: started_mysqld?
|
||||
import db.mysql
|
||||
|
||||
fn test_prep() {
|
||||
$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: '12345678'
|
||||
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']
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn test_stmt_bind_result_buffer_without_bind_res() {
|
||||
$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: '12345678'
|
||||
dbname: 'mysql'
|
||||
}
|
||||
|
||||
mut db := mysql.connect(config)!
|
||||
defer {
|
||||
db.close() or {}
|
||||
}
|
||||
|
||||
db.exec('drop table if exists stmt_result_buffer_test')!
|
||||
db.exec('create table if not exists stmt_result_buffer_test (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
value TEXT)')!
|
||||
db.exec_param('insert into stmt_result_buffer_test (value) values (?)', 'hello')!
|
||||
|
||||
mut stmt := db.init_stmt('select value from stmt_result_buffer_test where id = ?')
|
||||
defer {
|
||||
stmt.close() or {}
|
||||
}
|
||||
stmt.prepare()!
|
||||
id := 1
|
||||
stmt.bind_int(&id)
|
||||
stmt.bind_params()!
|
||||
stmt.execute()!
|
||||
stmt.bind_result_buffer()!
|
||||
stmt.store_result()!
|
||||
first_fetch := stmt.fetch_stmt()!
|
||||
assert first_fetch != 100
|
||||
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: '12345678'
|
||||
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: '12345678'
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
module mysql
|
||||
|
||||
pub struct Result {
|
||||
pub:
|
||||
result &C.MYSQL_RES = unsafe { nil }
|
||||
}
|
||||
|
||||
pub struct Row {
|
||||
pub mut:
|
||||
vals []string
|
||||
}
|
||||
|
||||
pub struct Field {
|
||||
name string
|
||||
org_name string
|
||||
table string
|
||||
org_table string
|
||||
db string
|
||||
catalog string
|
||||
def string
|
||||
length int
|
||||
max_length int
|
||||
name_length u32
|
||||
org_name_length u32
|
||||
table_length u32
|
||||
org_table_length u32
|
||||
db_length u32
|
||||
catalog_length u32
|
||||
def_length u32
|
||||
flags u32
|
||||
decimals u32
|
||||
charsetnr u32
|
||||
type FieldType
|
||||
}
|
||||
|
||||
// fetch_row fetches the next row from a result.
|
||||
pub fn (r Result) fetch_row() &charptr {
|
||||
mut thread_guard := mysql_thread_guard() or { return unsafe { nil } }
|
||||
defer {
|
||||
thread_guard.release()
|
||||
}
|
||||
return C.mysql_fetch_row(r.result)
|
||||
}
|
||||
|
||||
// n_rows returns the number of rows from a result.
|
||||
pub fn (r Result) n_rows() u64 {
|
||||
mut thread_guard := mysql_thread_guard() or { return 0 }
|
||||
defer {
|
||||
thread_guard.release()
|
||||
}
|
||||
return C.mysql_num_rows(r.result)
|
||||
}
|
||||
|
||||
// n_fields returns the number of columns from a result.
|
||||
pub fn (r Result) n_fields() int {
|
||||
mut thread_guard := mysql_thread_guard() or { return 0 }
|
||||
defer {
|
||||
thread_guard.release()
|
||||
}
|
||||
return C.mysql_num_fields(r.result)
|
||||
}
|
||||
|
||||
// rows returns array of rows, each containing an array of values,
|
||||
// one for each column.
|
||||
pub fn (r Result) rows() []Row {
|
||||
mut thread_guard := mysql_thread_guard() or { return []Row{} }
|
||||
defer {
|
||||
thread_guard.release()
|
||||
}
|
||||
mut rows := []Row{}
|
||||
nr_cols := C.mysql_num_fields(r.result)
|
||||
for rr := C.mysql_fetch_row(r.result); rr; rr = C.mysql_fetch_row(r.result) {
|
||||
mut row := Row{}
|
||||
for i in 0 .. nr_cols {
|
||||
if unsafe { rr[i] == 0 } {
|
||||
row.vals << ''
|
||||
} else {
|
||||
row.vals << mystring(unsafe { &u8(rr[i]) })
|
||||
}
|
||||
}
|
||||
rows << row
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
// maps returns an array of maps, each containing a set of
|
||||
// field name: field value pairs.
|
||||
pub fn (r Result) maps() []map[string]string {
|
||||
mut array_map := []map[string]string{}
|
||||
rows := r.rows()
|
||||
fields := r.fields()
|
||||
for i in 0 .. rows.len {
|
||||
mut map_val := map[string]string{}
|
||||
for j in 0 .. fields.len {
|
||||
map_val[fields[j].name] = rows[i].vals[j]
|
||||
}
|
||||
array_map << map_val
|
||||
}
|
||||
return array_map
|
||||
}
|
||||
|
||||
// fields returns an array of fields/columns.
|
||||
// The definitions apply primarily for columns of results,
|
||||
// such as those produced by `SELECT` statements.
|
||||
pub fn (r Result) fields() []Field {
|
||||
mut thread_guard := mysql_thread_guard() or { return []Field{} }
|
||||
defer {
|
||||
thread_guard.release()
|
||||
}
|
||||
mut fields := []Field{}
|
||||
nr_cols := C.mysql_num_fields(r.result)
|
||||
orig_fields := C.mysql_fetch_fields(r.result)
|
||||
for i in 0 .. nr_cols {
|
||||
unsafe {
|
||||
fields << Field{
|
||||
name: mystring(orig_fields[i].name)
|
||||
org_name: mystring(orig_fields[i].org_name)
|
||||
table: mystring(orig_fields[i].table)
|
||||
org_table: mystring(orig_fields[i].org_table)
|
||||
db: mystring(orig_fields[i].db)
|
||||
catalog: mystring(orig_fields[i].catalog)
|
||||
def: resolve_nil_str(orig_fields[i].def)
|
||||
length: orig_fields.length
|
||||
max_length: orig_fields.max_length
|
||||
name_length: orig_fields.name_length
|
||||
org_name_length: orig_fields.org_name_length
|
||||
table_length: orig_fields.table_length
|
||||
org_table_length: orig_fields.org_table_length
|
||||
db_length: orig_fields.db_length
|
||||
catalog_length: orig_fields.catalog_length
|
||||
def_length: orig_fields.def_length
|
||||
flags: orig_fields.flags
|
||||
decimals: orig_fields.decimals
|
||||
charsetnr: orig_fields.charsetnr
|
||||
type: FieldType(orig_fields.type)
|
||||
}
|
||||
}
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
// str serializes the field.
|
||||
pub fn (f Field) str() string {
|
||||
return '
|
||||
{
|
||||
name: "${f.name}"
|
||||
org_name: "${f.org_name}"
|
||||
table: "${f.table}"
|
||||
org_table: "${f.org_table}"
|
||||
db: "${f.db}"
|
||||
catalog: "${f.catalog}"
|
||||
def: "${f.def}"
|
||||
length: ${f.length}
|
||||
max_length: ${f.max_length}
|
||||
name_length: ${f.name_length}
|
||||
org_name_length: ${f.org_name_length}
|
||||
table_length: ${f.table_length}
|
||||
org_table_length: ${f.org_table_length}
|
||||
db_length: ${f.db_length}
|
||||
catalog_length: ${f.catalog_length}
|
||||
def_length: ${f.def_length}
|
||||
flags: ${f.flags}
|
||||
decimals: ${f.decimals}
|
||||
charsetnr: ${f.charsetnr}
|
||||
type: ${f.type.str()}
|
||||
}
|
||||
'
|
||||
}
|
||||
|
||||
// free frees the memory used by a result.
|
||||
@[unsafe]
|
||||
pub fn (r &Result) free() {
|
||||
mut thread_guard := mysql_thread_guard() or { return }
|
||||
defer {
|
||||
thread_guard.release()
|
||||
}
|
||||
C.mysql_free_result(r.result)
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
module mysql
|
||||
|
||||
@[typedef]
|
||||
pub struct C.MYSQL_STMT {
|
||||
mysql &C.MYSQL
|
||||
stmt_id u32
|
||||
}
|
||||
|
||||
@[typedef]
|
||||
pub struct C.MYSQL_BIND {
|
||||
mut:
|
||||
buffer_type int
|
||||
buffer voidptr
|
||||
buffer_length u32
|
||||
length &u32
|
||||
is_null &bool
|
||||
}
|
||||
|
||||
const mysql_type_decimal = C.MYSQL_TYPE_DECIMAL
|
||||
const mysql_type_tiny = C.MYSQL_TYPE_TINY
|
||||
const mysql_type_short = C.MYSQL_TYPE_SHORT
|
||||
const mysql_type_long = C.MYSQL_TYPE_LONG
|
||||
const mysql_type_float = C.MYSQL_TYPE_FLOAT
|
||||
const mysql_type_double = C.MYSQL_TYPE_DOUBLE
|
||||
const mysql_type_null = C.MYSQL_TYPE_NULL
|
||||
const mysql_type_timestamp = C.MYSQL_TYPE_TIMESTAMP
|
||||
const mysql_type_longlong = C.MYSQL_TYPE_LONGLONG
|
||||
const mysql_type_int24 = C.MYSQL_TYPE_INT24
|
||||
const mysql_type_date = C.MYSQL_TYPE_DATE
|
||||
const mysql_type_time = C.MYSQL_TYPE_TIME
|
||||
const mysql_type_datetime = C.MYSQL_TYPE_DATETIME
|
||||
const mysql_type_year = C.MYSQL_TYPE_YEAR
|
||||
const mysql_type_varchar = C.MYSQL_TYPE_VARCHAR
|
||||
const mysql_type_bit = C.MYSQL_TYPE_BIT
|
||||
const mysql_type_timestamp22 = C.MYSQL_TYPE_TIMESTAMP
|
||||
const mysql_type_json = C.MYSQL_TYPE_JSON
|
||||
const mysql_type_newdecimal = C.MYSQL_TYPE_NEWDECIMAL
|
||||
const mysql_type_enum = C.MYSQL_TYPE_ENUM
|
||||
const mysql_type_set = C.MYSQL_TYPE_SET
|
||||
const mysql_type_tiny_blob = C.MYSQL_TYPE_TINY_BLOB
|
||||
const mysql_type_medium_blob = C.MYSQL_TYPE_MEDIUM_BLOB
|
||||
const mysql_type_long_blob = C.MYSQL_TYPE_LONG_BLOB
|
||||
const mysql_type_blob = C.MYSQL_TYPE_BLOB
|
||||
const mysql_type_var_string = C.MYSQL_TYPE_VAR_STRING
|
||||
const mysql_type_string = C.MYSQL_TYPE_STRING
|
||||
const mysql_type_geometry = C.MYSQL_TYPE_GEOMETRY
|
||||
const mysql_no_data = C.MYSQL_NO_DATA
|
||||
|
||||
fn C.mysql_stmt_init(&C.MYSQL) &C.MYSQL_STMT
|
||||
fn C.mysql_stmt_prepare(&C.MYSQL_STMT, const_query charptr, u32) i32
|
||||
fn C.mysql_stmt_bind_param(&C.MYSQL_STMT, &C.MYSQL_BIND) bool
|
||||
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
|
||||
fn C.mysql_stmt_bind_result(&C.MYSQL_STMT, &C.MYSQL_BIND) bool
|
||||
fn C.mysql_stmt_fetch(&C.MYSQL_STMT) i32
|
||||
fn C.mysql_stmt_next_result(&C.MYSQL_STMT) i32
|
||||
fn C.mysql_stmt_store_result(&C.MYSQL_STMT) i32
|
||||
fn C.mysql_stmt_fetch_column(&C.MYSQL_STMT, &C.MYSQL_BIND, u32, u64) i32
|
||||
|
||||
pub struct Stmt {
|
||||
stmt &C.MYSQL_STMT = &C.MYSQL_STMT(unsafe { nil })
|
||||
query string
|
||||
mut:
|
||||
binds []C.MYSQL_BIND
|
||||
res []C.MYSQL_BIND
|
||||
auto_res_lengths []u32
|
||||
auto_res_is_null []bool
|
||||
}
|
||||
|
||||
// str returns a text representation of the given mysql statement `s`.
|
||||
pub fn (s &Stmt) str() string {
|
||||
return 'mysql.Stmt{ stmt: ${voidptr(s.stmt):x}, query: `${s.query}`, binds.len: ${s.binds.len}, res.len: ${s.res.len} }'
|
||||
}
|
||||
|
||||
// init_stmt creates a new statement, given the `query`.
|
||||
pub fn (db DB) init_stmt(query string) Stmt {
|
||||
return Stmt{
|
||||
stmt: C.mysql_stmt_init(db.conn)
|
||||
query: query
|
||||
binds: []C.MYSQL_BIND{}
|
||||
}
|
||||
}
|
||||
|
||||
// prepare a statement for execution.
|
||||
pub fn (stmt Stmt) prepare() ! {
|
||||
result := C.mysql_stmt_prepare(stmt.stmt, charptr(stmt.query.str), stmt.query.len)
|
||||
|
||||
if result != 0 && stmt.get_error_msg() != '' {
|
||||
return stmt.error(result)
|
||||
}
|
||||
}
|
||||
|
||||
// bind_params binds all the parameters in `stmt`.
|
||||
pub fn (stmt Stmt) bind_params() ! {
|
||||
result := C.mysql_stmt_bind_param(stmt.stmt, unsafe { &C.MYSQL_BIND(stmt.binds.data) })
|
||||
|
||||
if result && stmt.get_error_msg() != '' {
|
||||
return stmt.error(1)
|
||||
}
|
||||
}
|
||||
|
||||
// execute executes the given `stmt` and waits for the result.
|
||||
pub fn (stmt Stmt) execute() !int {
|
||||
result := C.mysql_stmt_execute(stmt.stmt)
|
||||
|
||||
if result != 0 && stmt.get_error_msg() != '' {
|
||||
return stmt.error(result)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// next retrieves the next available result from the execution of `stmt`
|
||||
pub fn (stmt Stmt) next() !int {
|
||||
result := C.mysql_stmt_next_result(stmt.stmt)
|
||||
|
||||
if result != 0 && stmt.get_error_msg() != '' {
|
||||
return stmt.error(result)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// gen_metadata executes mysql_stmt_result_metadata over the given `stmt`
|
||||
// It requires that the statement has produced a result set, since the metadata will be for that result set.
|
||||
// See https://dev.mysql.com/doc/c-api/5.7/en/mysql-stmt-result-metadata.html
|
||||
pub fn (stmt Stmt) gen_metadata() &C.MYSQL_RES {
|
||||
return C.mysql_stmt_result_metadata(stmt.stmt)
|
||||
}
|
||||
|
||||
// fetch_fields retrieves the fields from the metadata result of the execution of `stmt`.
|
||||
// See https://dev.mysql.com/doc/c-api/5.7/en/mysql-fetch-fields.html
|
||||
// See also Result.n_fields for the size of the returned C array.
|
||||
pub fn (stmt Stmt) fetch_fields(res &C.MYSQL_RES) &C.MYSQL_FIELD {
|
||||
return C.mysql_fetch_fields(res)
|
||||
}
|
||||
|
||||
// fetch_stmt fetches the next row in the result set. It returns the status of the execution of mysql_stmt_fetch .
|
||||
// See https://dev.mysql.com/doc/c-api/5.7/en/mysql-stmt-fetch.html
|
||||
pub fn (stmt Stmt) fetch_stmt() !int {
|
||||
result := C.mysql_stmt_fetch(stmt.stmt)
|
||||
|
||||
if result !in [0, 100] && stmt.get_error_msg() != '' {
|
||||
return stmt.error(result)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// close disposes the prepared `stmt`. The statement becomes invalid, and should not be used anymore after this call.
|
||||
// If the current statement has pending or unread results, this method cancels them too.
|
||||
// See https://dev.mysql.com/doc/c-api/5.7/en/mysql-stmt-close.html
|
||||
pub fn (stmt Stmt) close() ! {
|
||||
if !C.mysql_stmt_close(stmt.stmt) && stmt.get_error_msg() != '' {
|
||||
return stmt.error(1)
|
||||
}
|
||||
|
||||
if !C.mysql_stmt_free_result(stmt.stmt) && stmt.get_error_msg() != '' {
|
||||
return stmt.error(1)
|
||||
}
|
||||
}
|
||||
|
||||
fn (stmt Stmt) get_error_msg() string {
|
||||
return get_stmt_error_msg(stmt.stmt)
|
||||
}
|
||||
|
||||
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})'
|
||||
code: code
|
||||
}
|
||||
}
|
||||
|
||||
fn (stmt Stmt) get_field_count() u16 {
|
||||
return C.mysql_stmt_field_count(stmt.stmt)
|
||||
}
|
||||
|
||||
// bind_bool binds a single boolean value to the statement `stmt`
|
||||
pub fn (mut stmt Stmt) bind_bool(b &bool) {
|
||||
stmt.bind(mysql_type_tiny, b, 0)
|
||||
}
|
||||
|
||||
// bind_byte binds a single byte value to the statement `stmt`
|
||||
pub fn (mut stmt Stmt) bind_byte(b &u8) {
|
||||
stmt.bind(mysql_type_tiny, b, 0)
|
||||
}
|
||||
|
||||
// bind_u8 binds a single u8 value to the statement `stmt`
|
||||
pub fn (mut stmt Stmt) bind_u8(b &u8) {
|
||||
stmt.bind(mysql_type_tiny, b, 0)
|
||||
}
|
||||
|
||||
// bind_i8 binds a single i8 value to the statement `stmt`
|
||||
pub fn (mut stmt Stmt) bind_i8(b &i8) {
|
||||
stmt.bind(mysql_type_tiny, b, 0)
|
||||
}
|
||||
|
||||
// bind_i16 binds a single i16 value to the statement `stmt`
|
||||
pub fn (mut stmt Stmt) bind_i16(b &i16) {
|
||||
stmt.bind(mysql_type_short, b, 0)
|
||||
}
|
||||
|
||||
// bind_u16 binds a single u16 value to the statement `stmt`
|
||||
pub fn (mut stmt Stmt) bind_u16(b &u16) {
|
||||
stmt.bind(mysql_type_short, b, 0)
|
||||
}
|
||||
|
||||
// bind_int binds a single int value to the statement `stmt`
|
||||
pub fn (mut stmt Stmt) bind_int(b &int) {
|
||||
stmt.bind(mysql_type_long, b, 0)
|
||||
}
|
||||
|
||||
// bind_u32 binds a single u32 value to the statement `stmt`
|
||||
pub fn (mut stmt Stmt) bind_u32(b &u32) {
|
||||
stmt.bind(mysql_type_long, b, 0)
|
||||
}
|
||||
|
||||
// bind_i64 binds a single i64 value to the statement `stmt`
|
||||
pub fn (mut stmt Stmt) bind_i64(b &i64) {
|
||||
stmt.bind(mysql_type_longlong, b, 0)
|
||||
}
|
||||
|
||||
// bind_u64 binds a single u64 value to the statement `stmt`
|
||||
pub fn (mut stmt Stmt) bind_u64(b &u64) {
|
||||
stmt.bind(mysql_type_longlong, b, 0)
|
||||
}
|
||||
|
||||
// bind_f32 binds a single f32 value to the statement `stmt`
|
||||
pub fn (mut stmt Stmt) bind_f32(b &f32) {
|
||||
stmt.bind(mysql_type_float, b, 0)
|
||||
}
|
||||
|
||||
// bind_f64 binds a single f64 value to the statement `stmt`
|
||||
pub fn (mut stmt Stmt) bind_f64(b &f64) {
|
||||
stmt.bind(mysql_type_double, b, 0)
|
||||
}
|
||||
|
||||
// bind_text binds a single string value to the statement `stmt`
|
||||
pub fn (mut stmt Stmt) bind_text(b string) {
|
||||
stmt.bind(mysql_type_string, b.str, u32(b.len))
|
||||
}
|
||||
|
||||
// bind_null binds a single NULL value to the statement `stmt`
|
||||
pub fn (mut stmt Stmt) bind_null() {
|
||||
stmt.binds << C.MYSQL_BIND{
|
||||
buffer_type: mysql_type_null
|
||||
length: 0
|
||||
is_null: 0
|
||||
}
|
||||
}
|
||||
|
||||
// bind binds a single value pointed by `buffer`, to the statement `stmt`. The buffer length must be passed as well in `buf_len`.
|
||||
// Note: it is more convenient to use one of the other bind_XYZ methods.
|
||||
pub fn (mut stmt Stmt) bind(typ int, buffer voidptr, buf_len u32) {
|
||||
stmt.binds << C.MYSQL_BIND{
|
||||
buffer_type: typ
|
||||
buffer: buffer
|
||||
buffer_length: buf_len
|
||||
length: 0
|
||||
is_null: 0
|
||||
}
|
||||
}
|
||||
|
||||
// bind_res will store one result in the statement `stmt`
|
||||
pub fn (mut stmt Stmt) bind_res(fields &C.MYSQL_FIELD, dataptr []&u8, lengths []u32, is_null []bool, num_fields int) {
|
||||
stmt.auto_res_lengths = []u32{}
|
||||
stmt.auto_res_is_null = []bool{}
|
||||
if num_fields <= 0 {
|
||||
stmt.res = []C.MYSQL_BIND{}
|
||||
return
|
||||
}
|
||||
stmt.res = []C.MYSQL_BIND{cap: num_fields}
|
||||
for i in 0 .. num_fields {
|
||||
stmt.res << C.MYSQL_BIND{
|
||||
buffer_type: unsafe { fields[i].type }
|
||||
buffer: dataptr[i]
|
||||
length: &lengths[i]
|
||||
is_null: &is_null[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut stmt Stmt) ensure_default_result_binds() {
|
||||
if stmt.res.len > 0 {
|
||||
return
|
||||
}
|
||||
num_fields := int(stmt.get_field_count())
|
||||
if num_fields <= 0 {
|
||||
return
|
||||
}
|
||||
stmt.auto_res_lengths = []u32{len: num_fields}
|
||||
stmt.auto_res_is_null = []bool{len: num_fields}
|
||||
stmt.res = []C.MYSQL_BIND{cap: num_fields}
|
||||
for i in 0 .. num_fields {
|
||||
stmt.res << C.MYSQL_BIND{
|
||||
buffer_type: mysql_type_string
|
||||
buffer: 0
|
||||
buffer_length: 0
|
||||
length: unsafe { &stmt.auto_res_lengths[i] }
|
||||
is_null: unsafe { &stmt.auto_res_is_null[i] }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// bind_result_buffer binds one result value, by calling mysql_stmt_bind_result .
|
||||
// See https://dev.mysql.com/doc/c-api/8.0/en/mysql-stmt-bind-result.html
|
||||
pub fn (mut stmt Stmt) bind_result_buffer() ! {
|
||||
stmt.ensure_default_result_binds()
|
||||
if stmt.res.len == 0 {
|
||||
return
|
||||
}
|
||||
result := C.mysql_stmt_bind_result(stmt.stmt, unsafe { &C.MYSQL_BIND(stmt.res.data) })
|
||||
|
||||
if result && stmt.get_error_msg() != '' {
|
||||
return stmt.error(1)
|
||||
}
|
||||
}
|
||||
|
||||
// store_result will *buffer the complete result set* from the execution of `stmt` *on the client side*.
|
||||
// Note: result sets are produced by calling mysql_stmt_execute() to executed prepared statements for SQL
|
||||
// statements such as SELECT, SHOW, DESCRIBE, and EXPLAIN.
|
||||
// By default, result sets for successfully executed prepared statements are *not buffered on the client*,
|
||||
// and mysql_stmt_fetch() fetches them one at a time from the server.
|
||||
// Note 2: call store_result, *after* binding data buffers with bind_result_buffer,
|
||||
// and *before* calling fetch_stmt to fetch rows.
|
||||
// See https://dev.mysql.com/doc/c-api/8.0/en/mysql-stmt-store-result.html
|
||||
pub fn (mut stmt Stmt) store_result() ! {
|
||||
result := C.mysql_stmt_store_result(stmt.stmt)
|
||||
|
||||
if result != 0 && stmt.get_error_msg() != '' {
|
||||
return stmt.error(result)
|
||||
}
|
||||
}
|
||||
|
||||
// fetch_column fetches one column from the current result set row.
|
||||
// `bind` provides the buffer where data should be placed.
|
||||
// It should be set up the same way as for `mysql_stmt_bind_result()`.
|
||||
// `column` indicates which column to fetch. The first column is numbered 0.
|
||||
pub fn (mut stmt Stmt) fetch_column(bind &C.MYSQL_BIND, column int) ! {
|
||||
result := C.mysql_stmt_fetch_column(stmt.stmt, bind, column, 0)
|
||||
|
||||
if result != 0 && stmt.get_error_msg() != '' {
|
||||
return stmt.error(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
module mysql
|
||||
|
||||
// get_error_msg returns error message from MySQL instance.
|
||||
fn get_error_msg(conn &C.MYSQL) string {
|
||||
return clone_mysql_cstring(C.mysql_error(conn))
|
||||
}
|
||||
|
||||
// get_errno returns error number from MySQL instance.
|
||||
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 clone_mysql_cstring(&u8(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)
|
||||
}
|
||||
|
||||
@[inline]
|
||||
fn clone_mysql_cstring(ptr &u8) string {
|
||||
if isnil(ptr) {
|
||||
return ''
|
||||
}
|
||||
return unsafe { tos_clone(ptr) }
|
||||
}
|
||||
|
||||
// resolve_nil_str returns an empty string if passed value is a nil pointer.
|
||||
fn resolve_nil_str(ptr &u8) string {
|
||||
return clone_mysql_cstring(ptr)
|
||||
}
|
||||
|
||||
@[inline]
|
||||
fn mystring(b &u8) string {
|
||||
return clone_mysql_cstring(b)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// vtest build: started_mysqld?
|
||||
module mysql
|
||||
|
||||
import json
|
||||
|
||||
struct MysqlErrorPayload {
|
||||
data string
|
||||
}
|
||||
|
||||
fn test_clone_mysql_cstring_keeps_json_encodable_error_text() {
|
||||
mut backing :=
|
||||
"Incorrect string value: 'undefined' for function uuid_to_bin; code: 1411".bytes()
|
||||
backing << u8(0)
|
||||
msg := clone_mysql_cstring(backing.data)
|
||||
|
||||
backing[0] = 0xff
|
||||
|
||||
assert msg == "Incorrect string value: 'undefined' for function uuid_to_bin; code: 1411"
|
||||
assert json.encode(MysqlErrorPayload{
|
||||
data: msg
|
||||
}) == '{"data":"Incorrect string value: \'undefined\' for function uuid_to_bin; code: 1411"}'
|
||||
}
|
||||
Reference in new issue
Block a user