Initial project import

This commit is contained in:
Agila Forge Importer committed 2026-07-13 15:20:32 +00:00
commit 23f2d29e32
11957 files changed
+1754577

No files matched your search

+113
View File
@@ -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()
```
+181
View File
@@ -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)
+34
View File
@@ -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"
+12
View File
@@ -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"
+26
View File
@@ -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)
+94
View File
@@ -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
+12
View File
@@ -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
+361
View File
@@ -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()
}
+247
View File
@@ -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
}
}
+550
View File
@@ -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
}
+38
View File
@@ -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 }
}
}
+169
View File
@@ -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
}
+178
View File
@@ -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)
}
+365
View File
@@ -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)
}
}
+39
View File
@@ -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)
}
+22
View File
@@ -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"}'
}