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

+15
View File
@@ -0,0 +1,15 @@
## Description
`db` is a namespace that contains several useful modules
for operating with databases (SQLite, MySQL, MSQL, etc.)
## Cross-driver consistency helpers
`db.pg` and `db.mysql` accept both `user` and `username` in their `Config` structs.
`db.pg`, `db.mysql`, and `db.sqlite` rows expose `row.val(index)` and `row.values()`
for direct string access. In `db.pg`, SQL `NULL` remains available through
`row.val_opt(index)`.
`db.pg`, `db.mysql`, and `db.sqlite` also expose `exec_param2(...)` as a convenience
wrapper around their parameterized query helpers.
+98
View File
@@ -0,0 +1,98 @@
# SQL Server ODBC
* This module wraps the ODBC C API for use with SQL Server.
* It can also be used with any ODBC data source by passing a raw ODBC connection string.
## Scope
* `db.mssql` can connect to any ODBC data source when `mssql.open(...)` or
`Connection.connect(...)` receives a valid ODBC connection string.
* `Config` can build connection strings from `driver`, `server`, `port`, `uid`/`user`,
`pwd`/`password`, `dbname`, `dsn`, and extra ODBC options.
* For non-MSSQL ODBC sources, pass a DSN or raw ODBC string directly.
## Dependencies
* ODBC driver manager development headers/libraries (`sql.h`, `sqlext.h`).
* Linux:
* Install unixODBC development packages (`unixodbc`, `unixodbc-dev`, or distro equivalent).
* macOS:
* Recommended: install unixODBC + pkg-config:
`brew install unixodbc pkg-config`
* Then install your DB vendor ODBC driver (for SQL Server: `msodbcsql18`).
* Details: see the Microsoft SQL Server ODBC driver installation guide.
* Windows:
* `odbc32` is included with the Windows SDK on most systems.
* Details:
https://learn.microsoft.com/en-us/sql/connect/odbc/microsoft-odbc-driver-for-sql-server
## Windows Notes
### Using `msvc`
* Make sure `cl.exe` of `msvc` is accessible from command line.
You can run `v` commands in `Visual Studio 2019 Developer Command Prompt` to be safe.
* C Headers and dlls can be automatically resolved by `msvc`.
### Using `tcc`
* Copy those headers to `@VEXEROOT\thirdparty\mssql\include`.
The version number `10.0.18362.0` might differ on your system.
Command Prompt commands:
```cmd
copy "C:\Program Files (x86)\Windows Kits\10\Include\10.0.18362.0\um\sql.h" thirdparty\mssql\include
copy ^
"C:\Program Files (x86)\Windows Kits\10\Include\10.0.18362.0\um\sqlext.h" ^
thirdparty\mssql\include
copy ^
"C:\Program Files (x86)\Windows Kits\10\Include\10.0.18362.0\um\sqltypes.h" ^
thirdparty\mssql\include
copy ^
"C:\Program Files (x86)\Windows Kits\10\Include\10.0.18362.0\um\sqlucode.h" ^
thirdparty\mssql\include
copy ^
"C:\Program Files (x86)\Windows Kits\10\Include\10.0.18362.0\shared\sal.h" ^
thirdparty\mssql\include
copy ^
"C:\Program Files (x86)\Windows Kits\10\Include\10.0.18362.0\shared\concurrencysal.h" ^
thirdparty\mssql\include
```
* dlls can be automatically resolved by `tcc`
## TODO
* Support ORM
## Usage
```v ignore
import db.mssql
fn test_example() ? {
// connect to server
mut conn := mssql.connect(
driver: 'ODBC Driver 18 for SQL Server'
server: '127.0.0.1'
port: 1433
user: '<your username>'
password: '<your password>'
dbname: 'master'
options: {
'Encrypt': 'yes'
'TrustServerCertificate': 'yes'
}
)?
defer {
conn.close()
}
// get current db name
mut query := 'SELECT DB_NAME()'
mut res := conn.query(query)?
assert res == mssql.Result{
rows: [mssql.Row{
vals: ['master']
}]
num_rows_affected: -1
}
}
```
You can also connect with a raw DSN or ODBC string:
```v ignore
mut conn := mssql.open('DSN=Reporting;Trusted_Connection=Yes')?
```
+23
View File
@@ -0,0 +1,23 @@
module mssql
$if $pkgconfig('odbc') {
#pkgconfig --cflags --libs odbc
} $else {
#flag -lodbc
// Common fallback paths when unixODBC is installed via package managers on macOS.
#flag darwin -I/opt/homebrew/opt/unixodbc/include
#flag darwin -L/opt/homebrew/opt/unixodbc/lib
#flag darwin -I/usr/local/opt/unixodbc/include
#flag darwin -L/usr/local/opt/unixodbc/lib
#flag darwin -I/opt/local/include
#flag darwin -L/opt/local/lib
}
$if tinyc {
// tcc does not search system include paths by default; add /usr/include so
// that it can find the unixODBC headers (sql.h, sqlext.h) on Linux.
#flag linux -I/usr/include
}
#include <sql.h>
#include <sqlext.h>
+12
View File
@@ -0,0 +1,12 @@
module mssql
// mssql module does not support tcc on windows
// odbc32 lib comes with windows sdk and does not need to be installed separately.
// v builder for msvc can resolve the sdk includes search path, so no need to repeat here.
#flag windows -lodbc32
// Special handling of sql headers on windows.
// Source is in v third party folder.
#flag windows -I@VEXEROOT/thirdparty/mssql/include
#include <mssql.h>
+44
View File
@@ -0,0 +1,44 @@
module mssql
type C.SQLCHAR = u8
type C.SQLHDBC = voidptr
type C.SQLHENV = voidptr
type C.SQLHANDLE = voidptr
type C.SQLHSTMT = voidptr
type C.SQLHWND = voidptr
type C.SQLINTEGER = int
type C.SQLLEN = isize
type C.SQLPOINTER = voidptr
type C.SQLRETURN = i16
type C.SQLSMALLINT = i16
type C.SQLUSMALLINT = u16
fn C.SQLAllocHandle(handle_type C.SQLSMALLINT, input_handle C.SQLHANDLE, output_handle &C.SQLHANDLE) C.SQLRETURN
fn C.SQLSetEnvAttr(environment_handle C.SQLHENV, attribute C.SQLINTEGER, value C.SQLPOINTER, string_length C.SQLINTEGER) C.SQLRETURN
fn C.SQLGetDiagRec(handle_type C.SQLSMALLINT, handle C.SQLHANDLE, rec_number C.SQLSMALLINT, sql_state &C.SQLCHAR,
native_error &C.SQLINTEGER, message_text &C.SQLCHAR, buffer_length C.SQLSMALLINT, text_length &C.SQLSMALLINT) C.SQLRETURN
fn C.SQLSetConnectAttr(connection_handle C.SQLHDBC, attribute C.SQLINTEGER, value C.SQLPOINTER, string_length C.SQLINTEGER) C.SQLRETURN
fn C.SQLDriverConnect(hdbc C.SQLHDBC, hwnd C.SQLHWND, sz_conn_str_in &C.SQLCHAR, cb_conn_str_in C.SQLSMALLINT,
sz_conn_str_out &C.SQLCHAR, cb_conn_str_out_max C.SQLSMALLINT, pcb_conn_str_out &C.SQLSMALLINT, f_driver_completion C.SQLUSMALLINT) C.SQLRETURN
fn C.SQLDisconnect(connection_handle C.SQLHDBC) C.SQLRETURN
fn C.SQLExecDirect(statement_handle C.SQLHSTMT, statement_text &C.SQLCHAR, text_length C.SQLINTEGER) C.SQLRETURN
fn C.SQLBindCol(statement_handle C.SQLHSTMT, column_number C.SQLUSMALLINT, target_type C.SQLSMALLINT, target_value C.SQLPOINTER,
buffer_length C.SQLLEN, str_len_or_ind &C.SQLLEN) C.SQLRETURN
fn C.SQLFetch(statement_handle C.SQLHSTMT) C.SQLRETURN
fn C.SQLFreeHandle(handle_type C.SQLSMALLINT, handle C.SQLHANDLE) C.SQLRETURN
fn C.SQLNumResultCols(statement_handle C.SQLHSTMT, column_count &C.SQLSMALLINT) C.SQLRETURN
fn C.SQLColAttribute(statement_handle C.SQLHSTMT, column_number C.SQLUSMALLINT, field_identifier C.SQLUSMALLINT,
character_attribute C.SQLPOINTER, buffer_length C.SQLSMALLINT, string_length C.SQLSMALLINT, numeric_attribute &C.SQLLEN) C.SQLRETURN
fn C.SQLRowCount(statement_handle C.SQLHSTMT, row_count &C.SQLLEN) C.SQLRETURN
+96
View File
@@ -0,0 +1,96 @@
module mssql
@[params]
pub struct Config {
pub:
conn_str string
dsn string
driver string
server string
port int
uid string
user string
pwd string
password string
// if dbname empty, conn str will not contain Database info,
// and it is up to the server to choose which db to connect to.
dbname string
options map[string]string
}
// connect creates a new connection using the provided configuration.
pub fn connect(config Config) !Connection {
return open(config.get_conn_str())
}
// open creates a new connection using a raw ODBC connection string.
pub fn open(conn_str string) !Connection {
mut conn := Connection{}
conn.connect(conn_str)!
return conn
}
fn preferred_value(primary string, fallback string) string {
if primary != '' {
return primary
}
return fallback
}
fn needs_odbc_braces(value string) bool {
return value.contains_any(' \t\r\n;{}')
}
fn format_odbc_value(value string) string {
if value == '' {
return ''
}
if needs_odbc_braces(value) {
return '{' + value.replace('}', '}}') + '}'
}
return value
}
fn append_conn_part(mut parts []string, key string, value string) {
if value == '' {
return
}
parts << '${key}=${format_odbc_value(value)}'
}
// get_conn_str builds an ODBC connection string from the configured fields.
pub fn (cfg Config) get_conn_str() string {
if cfg.conn_str != '' {
return cfg.conn_str
}
mut parts := []string{}
append_conn_part(mut parts, 'DSN', cfg.dsn)
append_conn_part(mut parts, 'Driver', cfg.driver)
append_conn_part(mut parts, 'Server', cfg.server)
if cfg.port > 0 {
parts << 'Port=${cfg.port}'
}
append_conn_part(mut parts, 'UID', preferred_value(cfg.uid, cfg.user))
append_conn_part(mut parts, 'PWD', preferred_value(cfg.pwd, cfg.password))
if cfg.dbname != '' {
append_conn_part(mut parts, 'Database', cfg.dbname)
}
if cfg.options.len > 0 {
mut option_keys := cfg.options.keys()
// Manual insertion sort avoids relying on the generic .sort() helper,
// which has tripped bounds-check builds in CI.
for i in 1 .. option_keys.len {
key := option_keys[i]
mut j := i
for j > 0 && option_keys[j - 1] > key {
option_keys[j] = option_keys[j - 1]
j--
}
option_keys[j] = key
}
for key in option_keys {
append_conn_part(mut parts, key, cfg.options[key])
}
}
return parts.join(';')
}
+129
View File
@@ -0,0 +1,129 @@
module mssql
pub struct Connection {
mut:
henv C.SQLHENV = C.SQLHENV(C.SQL_NULL_HENV) // Environment
hdbc C.SQLHDBC = C.SQLHDBC(C.SQL_NULL_HDBC) // Connection handle
pub mut:
conn_str string
}
// connect to db
pub fn (mut conn Connection) connect(conn_str string) !bool {
conn_str_c := unsafe { &C.SQLCHAR(conn_str.str) }
mut retcode := C.SQLRETURN(C.SQL_SUCCESS)
// Allocate environment handle
retcode = C.SQLAllocHandle(C.SQLSMALLINT(C.SQL_HANDLE_ENV), C.SQLHANDLE(C.SQL_NULL_HANDLE),
unsafe { &C.SQLHANDLE(&conn.henv) })
check_error(retcode, 'SQLAllocHandle(SQL_HANDLE_ENV)', C.SQLHANDLE(conn.henv),
C.SQLSMALLINT(C.SQL_HANDLE_ENV))!
// Set the ODBC version environment attribute
retcode = C.SQLSetEnvAttr(conn.henv, C.SQLINTEGER(C.SQL_ATTR_ODBC_VERSION),
C.SQLPOINTER(C.SQL_OV_ODBC3), C.SQLINTEGER(0))
check_error(retcode, 'SQLSetEnvAttr(SQL_ATTR_ODBC_VERSION)', C.SQLHANDLE(conn.henv),
C.SQLSMALLINT(C.SQL_HANDLE_ENV))!
// Allocate connection handle
retcode = C.SQLAllocHandle(C.SQLSMALLINT(C.SQL_HANDLE_DBC), C.SQLHANDLE(conn.henv),
unsafe { &C.SQLHANDLE(&conn.hdbc) })
check_error(retcode, 'SQLAllocHandle(SQL_HANDLE_DBC)', C.SQLHANDLE(conn.hdbc),
C.SQLSMALLINT(C.SQL_HANDLE_DBC))!
// Set login timeout to 5 seconds
retcode = C.SQLSetConnectAttr(conn.hdbc, C.SQLINTEGER(C.SQL_LOGIN_TIMEOUT), C.SQLPOINTER(5),
C.SQLINTEGER(0))
check_error(retcode, 'SQLSetConnectAttr(SQL_LOGIN_TIMEOUT)', C.SQLHANDLE(conn.hdbc),
C.SQLSMALLINT(C.SQL_HANDLE_DBC))!
// Connect to data source
mut outstr := [1024]char{}
mut outstrlen := C.SQLSMALLINT(0)
retcode = C.SQLDriverConnect(conn.hdbc, C.SQLHWND(0), conn_str_c, C.SQLSMALLINT(C.SQL_NTS),
&C.SQLCHAR(&outstr[0]), C.SQLSMALLINT(sizeof(outstr)), &outstrlen,
C.SQLUSMALLINT(C.SQL_DRIVER_NOPROMPT))
check_error(retcode, 'SQLDriverConnect()', C.SQLHANDLE(conn.hdbc),
C.SQLSMALLINT(C.SQL_HANDLE_DBC))!
conn.conn_str = conn_str
return true
}
// close - closes the connection.
pub fn (mut conn Connection) close() {
// Connection
if conn.hdbc != C.SQLHDBC(C.SQL_NULL_HDBC) {
C.SQLDisconnect(conn.hdbc)
C.SQLFreeHandle(C.SQLSMALLINT(C.SQL_HANDLE_DBC), C.SQLHANDLE(conn.hdbc))
conn.hdbc = C.SQLHDBC(C.SQL_NULL_HDBC)
}
// Environment
if conn.henv != C.SQLHENV(C.SQL_NULL_HENV) {
C.SQLFreeHandle(C.SQLSMALLINT(C.SQL_HANDLE_ENV), C.SQLHANDLE(conn.henv))
conn.henv = C.SQLHENV(C.SQL_NULL_HENV)
}
}
// query executes a sql query
pub fn (mut conn Connection) query(q string) !Result {
mut hstmt := new_hstmt(conn.hdbc)!
defer {
hstmt.close()
}
hstmt.exec(q)!
affected := hstmt.retrieve_affected_rows()!
hstmt.prepare_read()!
raw_rows := hstmt.read_rows()!
mut res := Result{
rows: []Row{}
num_rows_affected: affected
}
for rr in raw_rows {
res.rows << Row{
vals: rr
}
}
return res
}
// check_error checks odbc return code and extract error string if available
fn check_error(e C.SQLRETURN, s string, h C.SQLHANDLE, t C.SQLSMALLINT) ! {
if e != C.SQLRETURN(C.SQL_SUCCESS) && e != C.SQLRETURN(C.SQL_SUCCESS_WITH_INFO) {
err_str := extract_error(s, h, t)
return error(err_str)
}
}
// extract_error extracts error string from odbc
fn extract_error(fnName string, handle C.SQLHANDLE, tp C.SQLSMALLINT) string {
mut err_str := fnName
mut i := 0
mut native_error := C.SQLINTEGER(0)
mut sql_state := [7]char{}
mut message_text := [256]char{}
mut text_length := C.SQLSMALLINT(0)
mut ret := C.SQLRETURN(C.SQL_SUCCESS)
for ret == C.SQLRETURN(C.SQL_SUCCESS) {
i++
ret = C.SQLGetDiagRec(tp, handle, C.SQLSMALLINT(i), &C.SQLCHAR(&sql_state[0]),
&native_error, &C.SQLCHAR(&message_text[0]), C.SQLSMALLINT(sizeof(message_text)),
&text_length)
// add driver error string
if ret == C.SQLRETURN(C.SQL_SUCCESS) || ret == C.SQLRETURN(C.SQL_SUCCESS_WITH_INFO) {
unsafe {
state_str := (&sql_state[0]).vstring()
native_error_code := int(native_error)
txt_str := (&message_text[0]).vstring()
err_str += '\n\todbc=${state_str}:${i}:${native_error_code}:${txt_str}'
}
}
}
return err_str
}
+115
View File
@@ -0,0 +1,115 @@
// vtest build: started_mssql?
module main
import db.mssql
import os
import time
const default_mssql_conn_str = 'Driver={FreeTDS};Server=127.0.0.1;Port=1433;UID=sa;PWD=Vlang12345678!;Database=master;TDS_Version=7.4;ClientCharset=UTF-8'
fn test_config_get_conn_str() {
assert mssql.Config{
driver: 'ODBC Driver 18 for SQL Server'
server: 'tcp:localhost'
uid: 'sa'
pwd: 'secret'
dbname: 'master'
}.get_conn_str() == 'Driver={ODBC Driver 18 for SQL Server};Server=tcp:localhost;UID=sa;PWD=secret;Database=master'
assert mssql.Config{
driver: 'FreeTDS'
server: '127.0.0.1'
port: 1433
user: 'sa'
password: 'secret'
options: {
'ClientCharset': 'UTF-8'
'TDS_Version': '7.4'
}
}.get_conn_str() == 'Driver=FreeTDS;Server=127.0.0.1;Port=1433;UID=sa;PWD=secret;ClientCharset=UTF-8;TDS_Version=7.4'
assert mssql.Config{
dsn: 'Reporting DB'
user: 'sa'
password: 'secret'
dbname: 'master'
options: {
'Encrypt': 'yes'
'TrustServerCertificate': 'yes'
}
}.get_conn_str() == 'DSN={Reporting DB};UID=sa;PWD=secret;Database=master;Encrypt=yes;TrustServerCertificate=yes'
assert mssql.Config{
conn_str: 'DSN=Accounting;Trusted_Connection=Yes'
driver: 'ignored'
}.get_conn_str() == 'DSN=Accounting;Trusted_Connection=Yes'
}
fn test_row_helpers() {
row := mssql.Row{
vals: ['1', 'alice']
}
assert row.val(0) == '1'
assert row.val(1) == 'alice'
assert row.values() == ['1', 'alice']
}
fn test_connection_and_query() {
$if !network ? {
eprintln('> Skipping test ${@FN}, since `-d network` is not passed.')
eprintln('> This test requires a working sql server running on localhost.')
return
}
mut conn := connect_with_retry()!
defer {
conn.close()
}
conn.query("if object_id('vlang_mssql_test', 'U') is not null drop table vlang_mssql_test") or {}
defer {
conn.query("if object_id('vlang_mssql_test', 'U') is not null drop table vlang_mssql_test") or {}
}
create_result := conn.query('create table vlang_mssql_test (
id int identity(1,1) primary key,
name varchar(32) not null
)')!
assert create_result.rows == []mssql.Row{}
first_insert := conn.query("insert into vlang_mssql_test (name) values ('alice')")!
assert first_insert.rows == []mssql.Row{}
assert first_insert.num_rows_affected == 1
second_insert := conn.query("insert into vlang_mssql_test (name) values ('bob')")!
assert second_insert.rows == []mssql.Row{}
assert second_insert.num_rows_affected == 1
result := conn.query('select id, name from vlang_mssql_test order by id')!
assert result.rows == [
mssql.Row{
vals: ['1', 'alice']
},
mssql.Row{
vals: ['2', 'bob']
},
]
}
fn connect_with_retry() !mssql.Connection {
conn_str := os.getenv_opt('VMSSQL_CONN_STR') or { default_mssql_conn_str }
mut last_err := 'could not connect to sql server'
for _ in 0 .. 30 {
mut conn := mssql.Connection{}
if _ := conn.connect(conn_str) {
// SQL Server can accept a connection before it is ready to execute statements.
if _ := conn.query('select 1') {
return conn
} else {
last_err = err.msg()
conn.close()
}
} else {
last_err = err.msg()
}
time.sleep(2 * time.second)
}
return error(last_err)
}
+23
View File
@@ -0,0 +1,23 @@
module mssql
pub struct Row {
pub mut:
vals []string
}
// val returns the value at `index`.
pub fn (row Row) val(index int) string {
return row.vals[index]
}
// values returns all row values.
pub fn (row Row) values() []string {
return row.vals.clone()
}
pub struct Result {
pub mut:
rows []Row
// the number of rows affected by sql statement
num_rows_affected int
}
+133
View File
@@ -0,0 +1,133 @@
module mssql
// HStmt is handle for sql statement
struct HStmt {
mut:
// db connection reference. Owner is Connection struct.
hdbc C.SQLHDBC = C.SQLHDBC(C.SQL_NULL_HDBC)
// statement handle
hstmt C.SQLHSTMT = C.SQLHSTMT(C.SQL_NULL_HSTMT)
// fields used for computation
column_count int = -1
// columns
buffers [][]char
// indicators for each column. Use a V pointer-sized integer to avoid arrays
// of C typedefs in generated code while still matching SQLLEN width.
indicators []isize
}
// new_hstmt constructs a new statement handle
fn new_hstmt(hdbc C.SQLHDBC) !HStmt {
mut retcode := C.SQLRETURN(C.SQL_SUCCESS)
mut hstmt := C.SQLHSTMT(C.SQL_NULL_HSTMT)
// Allocate statement handle
retcode = C.SQLAllocHandle(C.SQLSMALLINT(C.SQL_HANDLE_STMT), C.SQLHANDLE(hdbc),
unsafe { &C.SQLHANDLE(&hstmt) })
check_error(retcode, 'SQLAllocHandle(SQL_HANDLE_STMT)', C.SQLHANDLE(hstmt),
C.SQLSMALLINT(C.SQL_HANDLE_STMT))!
return HStmt{
hdbc: hdbc
hstmt: hstmt
}
}
// close the statement handle
fn (mut h HStmt) close() {
// Deallocate handle
if h.hstmt != C.SQLHSTMT(C.SQL_NULL_HSTMT) {
// check error code?
C.SQLFreeHandle(C.SQLSMALLINT(C.SQL_HANDLE_STMT), C.SQLHANDLE(h.hstmt))
h.hstmt = C.SQLHSTMT(C.SQL_NULL_HSTMT)
}
}
// exec executes a Sql statement. Result is stored in odbc driver, and not yet read.
fn (h HStmt) exec(sql_ string) ! {
retcode := C.SQLExecDirect(h.hstmt, sql_.str, C.SQLINTEGER(C.SQL_NTS))
check_error(retcode, 'SQLExecDirect()', C.SQLHANDLE(h.hstmt), C.SQLSMALLINT(C.SQL_HANDLE_STMT))!
}
// retrieve_affected_rows returns number of rows affected/modified by the last operation. -1 if not applicable.
fn (h HStmt) retrieve_affected_rows() !int {
count_ret := C.SQLLEN(0)
retcode := C.SQLRowCount(h.hstmt, &count_ret)
check_error(retcode, 'SQLRowCount()', C.SQLHANDLE(h.hstmt), C.SQLSMALLINT(C.SQL_HANDLE_STMT))!
return int(count_ret)
}
fn (h HStmt) retrieve_column_count() !int {
mut retcode := C.SQLRETURN(C.SQL_SUCCESS)
col_count_buff := C.SQLSMALLINT(0)
retcode = C.SQLNumResultCols(h.hstmt, &col_count_buff)
check_error(retcode, 'SQLNumResultCols()', C.SQLHANDLE(h.hstmt),
C.SQLSMALLINT(C.SQL_HANDLE_STMT))!
return int(col_count_buff)
}
// allocate buffers and bind them to drivers
fn (mut h HStmt) prepare_read() ! {
mut retcode := C.SQLRETURN(C.SQL_SUCCESS)
column_count := h.retrieve_column_count()!
h.column_count = column_count // remember the count because read will need it
h.buffers = [][]char{len: h.column_count}
h.indicators = []isize{len: h.column_count}
for i := 0; i < h.column_count; i++ {
i_col := C.SQLUSMALLINT(i + 1) // col number starts with 1
size_ret := C.SQLLEN(0)
// find out buffer size needed to read data in this column
retcode = C.SQLColAttribute(h.hstmt, i_col, C.SQLUSMALLINT(C.SQL_DESC_LENGTH),
C.SQLPOINTER(0), C.SQLSMALLINT(0), C.SQLSMALLINT(0), &size_ret)
check_error(retcode, 'SQLColAttribute()', C.SQLHANDLE(h.hstmt),
C.SQLSMALLINT(C.SQL_HANDLE_STMT))!
// buffer allocation is the size + 1 to include termination char, since SQL_DESC_LENGTH does not include it.
allocate_size := size_ret + C.SQLLEN(1)
allocate_size_int := int(allocate_size)
buff := []char{len: allocate_size_int}
// bind the buffer
retcode = C.SQLBindCol(h.hstmt, C.SQLUSMALLINT(i_col), C.SQLSMALLINT(C.SQL_C_CHAR),
C.SQLPOINTER(&buff[0]), allocate_size, unsafe { &C.SQLLEN(&h.indicators[i]) })
check_error(retcode, 'SQLBindCol()', C.SQLHANDLE(h.hstmt), C.SQLSMALLINT(C.SQL_HANDLE_STMT))!
// record the buffer in HStmt
h.buffers[i] = buff
}
}
// fetch all rows
fn (h HStmt) read_rows() ![][]string {
mut retcode := C.SQLRETURN(C.SQL_SUCCESS)
mut res := [][]string{}
if h.column_count <= 0 {
// there is nothing in the driver to read from
return res
}
// Fetch and print each row of data until SQL_NO_DATA returned.
for {
mut row := []string{}
retcode = C.SQLFetch(h.hstmt)
if retcode == C.SQLRETURN(C.SQL_SUCCESS) || retcode == C.SQLRETURN(C.SQL_SUCCESS_WITH_INFO) {
// copy buffered result to res
for content in h.buffers {
row << unsafe { cstring_to_vstring(content.data) }
}
} else {
if retcode != C.SQLRETURN(C.SQL_NO_DATA) {
check_error(retcode, 'SQLFetch()', C.SQLHANDLE(h.hstmt),
C.SQLSMALLINT(C.SQL_HANDLE_STMT))!
} else {
break
}
}
res << row
}
return res
}
+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"}'
}
+34
View File
@@ -0,0 +1,34 @@
// vtest build: started_mysqld?
module main
import db.mysql
fn test_mysql_connection_user_aliases() {
assert mysql.Config{
user: 'alice'
}.connection_user()! == 'alice'
assert mysql.Config{
username: 'alice'
}.connection_user()! == 'alice'
assert mysql.Config{
user: 'alice'
username: 'alice'
}.connection_user()! == 'alice'
if _ := mysql.Config{
user: 'alice'
username: 'bob'
}.connection_user()
{
assert false
} else {
assert err.msg().contains('must match')
}
}
fn test_mysql_row_value_helpers() {
mysql_row := mysql.Row{
vals: ['hello', '']
}
assert mysql_row.val(0) == 'hello'
assert mysql_row.values() == ['hello', '']
}
+260
View File
@@ -0,0 +1,260 @@
## Description
`pg` is a wrapper for the PostgreSQL client library. It provides access to a PostgreSQL
database server.
Before you can use this module, you must first have PostgreSQL installed on your system.
To do this, find your OS and perform the actions listed.
> **Note**
> These instructions are meant only as a convenience. If your OS is not listed
> or you need extra help, [go here](https://www.postgresql.org/download/).
### Fedora 31
```
sudo dnf install postgresql-server postgresql-contrib
sudo systemctl enable postgresql # to autostart on startup
sudo systemctl start postgresql
```
### Ubuntu/Debian
```
sudo apt install postgresql postgresql-client
sudo systemctl enable postgresql # to autostart on startup
sudo systemctl start postgresql
```
### MacOSX (Homebrew)
```
brew install postgresql
brew services start postgresql
```
On newer Homebrew setups the formula and service name may be versioned
instead, for example `postgresql@18`. Use the exact name reported by
`brew info postgresql`.
### MacOSX (MacPorts)
```
gem install pg -- --with-pg-config=/opt/local/lib/postgresql[version number]/bin/pg_config
```
## Installing libpq-dev or its equivalent for your OS
**Ubuntu/Debian**: `sudo apt install libpq-dev`
**Red Hat Linux (RHEL)**: `yum install postgresql-devel`
**OpenSuse**: `zypper in postgresql-devel`
**ArchLinux**: `pacman -S postgresql-libs`
**FreeBSD**: `pkg install postgresql18-client`
**OpenBSD**: `pkg_add postgresql-client`
**Windows**:
The directory structure of the `@VEXEROOT/thirdparty/pg` folder will look like the following
after following all instructions. Create the `pg` folder yourself if it does not exist yet.
```
@VEXEROOT/thirdparty/pg
├───libpq
│ libpq-fe.h
│ pg_config.h
│ postgres_ext.h
│
└───win64
└───msvc
libpq.lib
```
Installation instructions are as follows
```
Download the latest PostgreSQL version from the official website
(currently https://www.enterprisedb.com/downloads/postgres-postgresql-downloads)
In one of the steps in the installer, tick the following boxes:
[X] PostgreSQL Server
[ ] pgAdmin 4
[ ] Stack Builder
[X] Command Line Tools
We need PostgreSQL Server because it brings with it the C header files
needed for building programs that link to `libpq.dll`.
After finishing installation, add the folder `C:/Program Files/PostgreSQL/<version>/bin` to PATH.
Any program that wants to use postgres client functionality require these DLLs found in `/bin`:
- libcrypto-3-x64.dll
- libiconv-2.dll
- libintl-9.dll
- libpq.dll
- libssl-3-x64.dll
- libwinpthread-1.dll
If you want to compile with MSVC, you will need to copy `C:/Program Files/PostgreSQL/<version>/bin/libpq.lib`
into the `@VEXEROOT/thirdparty/pg/win64/msvc` directory.
Navigate to `C:/Program Files/PostgreSQL/<version>/include`. There you will find the files:
- `libpq-fe.h`
- `pg_config.h`
- `postgres_ext.h`
Copy the header files into `@VEXEROOT/thirdparty/pg/libpq`. You can now compile programs using the `db.pg` module.
---
After building an executable that uses `db.pg`, you may want to distribute it to others
who might not have the postgres DLLs installed on their machine. All you need to do is to
make sure a copy of all the required DLLs are in the same folder as your executable.
```
## Getting Started with [PostgreSQL](https://www.postgresqltutorial.com/postgresql-getting-started)
Read this section to learn how to install and connect to PostgreSQL
[*Windows*](https://www.postgresqltutorial.com/install-postgresql);
[*Linux*](https://www.postgresqltutorial.com/postgresql-getting-started/install-postgresql-linux);
[*macOS*](https://www.postgresqltutorial.com/postgresql-getting-started/install-postgresql-macos).
When you use `pg.connect(pg.Config{ ... })`, empty `Config` fields are omitted from the
generated libpq connection string. That lets libpq defaults, `PGPASSWORD`, and `.pgpass`
apply when you do not set those fields in code.
## Thread Safety & Connection Pool
`pg.connect()` returns a `&DB` that is safe to share across V threads. Internally
`DB` holds a pool of `Conn` objects (one libpq `PGconn*` each); every method on
`DB` transparently checks a `Conn` out of the pool for the duration of the call
and returns it when done. This matches Go's `database/sql.DB` model.
```v ignore
mut db := pg.connect(pg.Config{ ... })!
defer { db.close() or {} }
// Pool defaults: unlimited open conns, 2 idle conns kept warm, no lifetime cap.
// Tune them like Go:
db.set_max_open_conns(50)
db.set_max_idle_conns(10)
db.set_conn_max_lifetime(30 * time.minute)
```
For operations that must run on the **same physical connection** — LISTEN/NOTIFY,
session-scoped prepared statements, manual transactions — pin a conn with
`db.conn()` or open a transaction with `db.begin()`:
```v ignore
// Pinned connection: returned to the pool when conn.close() is called.
mut c := db.conn()!
defer { c.close() or {} }
c.listen('my_channel')!
// Transaction: the conn is pinned for the lifetime of the Tx and released on
// commit() or rollback().
mut tx := db.begin()!
tx.exec('UPDATE accounts SET balance = balance - 100 WHERE id = 1')!
tx.exec('UPDATE accounts SET balance = balance + 100 WHERE id = 2')!
tx.commit()!
```
## Using Parameterized Queries
Parameterized queries (exec_param, etc.) in V require the use of the following syntax: ($n).
The number following the $ specifies which parameter from the argument array to use.
```v ignore
db.exec_param_many('INSERT INTO users (username, password) VALUES ($1, $2)', ['tom', 'securePassword'])!
db.exec_param('SELECT * FROM users WHERE username = ($1) limit 1', 'tom')!
```
## Using LISTEN/NOTIFY
PostgreSQL's LISTEN/NOTIFY mechanism allows you to build event-driven applications. One
connection can send notifications on a channel, and all connections listening on that
channel will receive them.
### Basic Usage
LISTEN/NOTIFY is session-scoped, so you must pin a `Conn` from the pool —
calling `db.listen()` would only listen on whichever pooled conn happens to
serve that one call.
```v ignore
import db.pg
fn main() {
mut db := pg.connect(pg.Config{ user: 'postgres', password: 'password', dbname: 'mydb' })!
defer { db.close() or {} }
mut c := db.conn()!
defer { c.close() or {} }
// Start listening on a channel
c.listen('my_channel')!
// From another connection or session, send a notification
c.notify('my_channel', 'Hello, World!')!
// Process incoming data from the server
c.consume_input()!
// Check for notifications
if notification := c.get_notification() {
println('Received notification on channel: ${notification.channel}')
println('Payload: ${notification.payload}')
println('From server process: ${notification.pid}')
}
// Stop listening
c.unlisten('my_channel')!
// Or unlisten from all channels
c.unlisten_all()!
}
```
### Event Loop with Polling
For real-time applications, you can use the socket file descriptor with select/poll:
```v ignore
import db.pg
import time
fn main() {
mut db := pg.connect(pg.Config{ user: 'postgres', password: 'password', dbname: 'mydb' })!
defer { db.close() or {} }
mut c := db.conn()!
defer { c.close() or {} }
c.listen('events')!
// Get socket fd for polling (useful with select/epoll)
socket_fd := c.socket()
println('Socket FD: ${socket_fd}')
// Simple polling loop
for {
c.consume_input()!
for {
notification := c.get_notification() or { break }
println('Event: ${notification.channel} - ${notification.payload}')
}
time.sleep(100 * time.millisecond)
}
}
```
### Available Methods
- `listen(channel string)` - Register to receive notifications on a channel
- `unlisten(channel string)` - Unregister from a specific channel
- `unlisten_all()` - Unregister from all channels
- `notify(channel string, payload string)` - Send a notification (payload can be empty)
- `consume_input()` - Read pending data from server (call before get_notification)
- `get_notification()` - Returns the next pending notification, or none for no notifications.
- `socket()` - Returns the connection's socket file descriptor for use with select/poll
+16
View File
@@ -0,0 +1,16 @@
#if !defined(PG_VERSION_NUM)
#error VERROR_MESSAGE PG_VERSION_NUM is not defined. Please install the development headers for PostgreSQL, they are usually in a package named libpq-dev
#endif
#if PG_VERSION_NUM < 100000
#define CONNECTION_CHECK_WRITABLE 9
#endif
#if PG_VERSION_NUM < 100000
#define CONNECTION_CONSUME 10
#endif
#if PG_VERSION_NUM < 120000
#define CONNECTION_GSS_STARTUP 11
#endif
+207
View File
@@ -0,0 +1,207 @@
module pg
import io
import time
// DB is a thread-safe handle to a PostgreSQL database, backed by a pool of
// `Conn` objects. It mirrors Go's `database/sql.DB` design: methods on `DB`
// transparently acquire a conn for the call, then release it back to the
// pool. For operations that must run on the same physical connection
// (LISTEN/NOTIFY, session-scoped prepared statements, manual transactions),
// use `db.conn()` to pin a conn or `db.begin()` to start a transaction.
pub struct DB {
mut:
pool &Pool = unsafe { nil }
}
// connect creates a new pool and opens an initial connection to verify the
// config works. The returned `&DB` is safe to share between threads.
pub fn connect(config Config, pcfg PoolConfig) !&DB {
return connect_with_conninfo(config.conninfo()!, pcfg)!
}
// connect_with_conninfo is the conninfo-string variant of `connect`.
pub fn connect_with_conninfo(conninfo string, pcfg PoolConfig) !&DB {
mut db := &DB{
pool: new_pool(conninfo, pcfg)
}
// Fail fast if the conninfo is wrong, rather than at first query.
probe := db.pool.acquire()!
db.pool.release(probe)
return db
}
// close shuts down the pool and tears down all idle connections.
// In-flight conns will be closed when released.
pub fn (mut db DB) close() ! {
if isnil(db.pool) {
return
}
db.pool.close()
}
// stats returns a snapshot of the pool state.
pub fn (mut db DB) stats() PoolStats {
return db.pool.stats()
}
// set_max_open_conns caps the total number of open connections.
// A value of 0 means unlimited (the default, like Go).
pub fn (mut db DB) set_max_open_conns(n int) {
db.pool.set_max_open(n)
}
// set_max_idle_conns caps the number of idle connections kept warm.
pub fn (mut db DB) set_max_idle_conns(n int) {
db.pool.set_max_idle(n)
}
// set_conn_max_lifetime sets the maximum amount of time a conn may be reused.
// A value of zero means conns are reused indefinitely.
pub fn (mut db DB) set_conn_max_lifetime(d time.Duration) {
db.pool.set_conn_max_lifetime(d)
}
// conn checks a conn out of the pool. The caller is responsible for calling
// `conn.close()` when done; failing to do so leaks the conn. Use this when
// you need session-bound operations like LISTEN/NOTIFY.
pub fn (mut db DB) conn() !&Conn {
return db.pool.acquire()
}
// validate borrows a conn from the pool and checks it is alive.
pub fn (mut db DB) validate() !bool {
mut c := db.pool.acquire()!
defer { c.close() or {} }
return c.validate()
}
// reset is a no-op kept for ORM compatibility.
pub fn (mut db DB) reset() ! {
}
// ---- exec/query helpers (acquire-use-release) ----
// exec runs `query` on a pooled conn and returns the rows.
pub fn (mut db DB) exec(query string) ![]Row {
mut c := db.pool.acquire()!
defer { c.close() or {} }
return c.exec(query)
}
// exec_no_null runs `query` and returns rows with no nullable fields.
pub fn (mut db DB) exec_no_null(query string) ![]RowNoNull {
mut c := db.pool.acquire()!
defer { c.close() or {} }
return c.exec_no_null(query)
}
// exec_result runs `query` and returns a `Result` (rows + column index).
pub fn (mut db DB) exec_result(query string) !Result {
mut c := db.pool.acquire()!
defer { c.close() or {} }
return c.exec_result(query)
}
// exec_one runs `query` and returns its first row.
pub fn (mut db DB) exec_one(query string) !Row {
mut c := db.pool.acquire()!
defer { c.close() or {} }
return c.exec_one(query)
}
// exec_param_many runs `query` with the given parameters.
pub fn (mut db DB) exec_param_many(query string, params []string) ![]Row {
mut c := db.pool.acquire()!
defer { c.close() or {} }
return c.exec_param_many(query, params)
}
// exec_param_many_result runs `query` with parameters and returns a `Result`.
pub fn (mut db DB) exec_param_many_result(query string, params []string) !Result {
mut c := db.pool.acquire()!
defer { c.close() or {} }
return c.exec_param_many_result(query, params)
}
// exec_param runs `query` with a single `$1` parameter.
pub fn (mut db DB) exec_param(query string, param string) ![]Row {
return db.exec_param_many(query, [param])
}
// exec_param2 runs `query` with two parameters (`$1`, `$2`).
pub fn (mut db DB) exec_param2(query string, param string, param2 string) ![]Row {
return db.exec_param_many(query, [param, param2])
}
// q_int runs `query` and returns the first column of the first row as int.
pub fn (mut db DB) q_int(query string) !int {
mut c := db.pool.acquire()!
defer { c.close() or {} }
return c.q_int(query)
}
// q_string runs `query` and returns the first column of the first row as string.
pub fn (mut db DB) q_string(query string) !string {
mut c := db.pool.acquire()!
defer { c.close() or {} }
return c.q_string(query)
}
// q_strings runs `query` and returns the full row set (alias of `exec`).
pub fn (mut db DB) q_strings(query string) ![]Row {
return db.exec(query)
}
// copy_expert runs a COPY command on a pooled conn.
pub fn (mut db DB) copy_expert(query string, mut file io.ReaderWriter) !int {
mut c := db.pool.acquire()!
defer { c.close() or {} }
return c.copy_expert(query, mut file)
}
// ---- prepared statements ----
//
// NOTE: prepared statements are session-scoped. Calling `prepare` on `DB`
// only registers the statement on the conn that happened to serve the call.
// Use `db.conn()` to pin a conn for prepare+exec_prepared cycles.
// prepare registers a prepared statement on a transient conn.
// For repeated use, pin a conn via `db.conn()`.
pub fn (mut db DB) prepare(name string, query string, num_params int) ! {
mut c := db.pool.acquire()!
defer { c.close() or {} }
return c.prepare(name, query, num_params)
}
// exec_prepared runs a previously-prepared statement.
pub fn (mut db DB) exec_prepared(name string, params []string) ![]Row {
mut c := db.pool.acquire()!
defer { c.close() or {} }
return c.exec_prepared(name, params)
}
// exec_prepared_result runs a previously-prepared statement and returns a `Result`.
pub fn (mut db DB) exec_prepared_result(name string, params []string) !Result {
mut c := db.pool.acquire()!
defer { c.close() or {} }
return c.exec_prepared_result(name, params)
}
// ---- transactions ----
// begin starts a new transaction and returns a `Tx` that pins a conn from
// the pool. The conn is released when `Tx.commit()` or `Tx.rollback()` is
// called. The default isolation level is REPEATABLE READ (matching the old
// single-conn API); pass `PQTransactionParam{ transaction_level: ... }` to
// override.
pub fn (mut db DB) begin(param PQTransactionParam) !&Tx {
mut c := db.pool.acquire()!
c.begin_on_conn(param) or {
c.close() or {}
return err
}
return &Tx{
conn: c
}
}
+171
View File
@@ -0,0 +1,171 @@
module pg
pub enum Oid {
t_bool = 16
t_bytea = 17
t_char = 18
t_name = 19
t_int8 = 20
t_int2 = 21
t_int2vector = 22
t_int4 = 23
t_regproc = 24
t_text = 25
t_oid = 26
t_tid = 27
t_xid = 28
t_cid = 29
t_vector = 30
t_pg_ddl_command = 32
t_pg_type = 71
t_pg_attribute = 75
t_pg_proc = 81
t_pg_class = 83
t_json = 114
t_xml = 142
t__xml = 143
t_pg_node_tree = 194
t__json = 199
t_smgr = 210
t_index_am_handler = 325
t_point = 600
t_lseg = 601
t_path = 602
t_box = 603
t_polygon = 604
t_line = 628
t__line = 629
t_cidr = 650
t__cidr = 651
t_float4 = 700
t_float8 = 701
t_abstime = 702
t_reltime = 703
t_tinterval = 704
t_unknown = 705
t_circle = 718
t__circle = 719
t_money = 790
t__money = 791
t_macaddr = 829
t_inet = 869
t__bool = 1000
t__bytea = 1001
t__char = 1002
t__name = 1003
t__int2 = 1005
t__int2vector = 1006
t__int4 = 1007
t__regproc = 1008
t__text = 1009
t__tid = 1010
t__xid = 1011
t__cid = 1012
t__vector = 1013
t__bpchar = 1014
t__varchar = 1015
t__int8 = 1016
t__point = 1017
t__lseg = 1018
t__path = 1019
t__box = 1020
t__float4 = 1021
t__float8 = 1022
t__abstime = 1023
t__reltime = 1024
t__tinterval = 1025
t__polygon = 1027
t__ = 1028
t_aclitem = 1033
t__aclitem = 1034
t__macaddr = 1040
t__inet = 1041
t_bpchar = 1042
t_varchar = 1043
t_date = 1082
t_time = 1083
t_timestamp = 1114
t__timestamp = 1115
t__date = 1182
t__time = 1183
t_timestamptz = 1184
t__timestamptz = 1185
t_interval = 1186
t__interval = 1187
t__numeric = 1231
t_pg_database = 1248
t__cstring = 1263
t_timetz = 1266
t__timetz = 1270
t_bit = 1560
t__bit = 1561
t_varbit = 1562
t__varbit = 1563
t_numeric = 1700
t_refcursor = 1790
t__refcursor = 2201
t_regprocedure = 2202
t_regoper = 2203
t_regoperator = 2204
t_regclass = 2205
t_regtype = 2206
t__regprocedure = 2207
t__regoper = 2208
t__regoperator = 2209
t__regclass = 2210
t__regtype = 2211
t_record = 2249
t_cstring = 2275
t_any = 2276
t_anyarray = 2277
t_v = 2278
t_trigger = 2279
t_language_handler = 2280
t_internal = 2281
t_opaque = 2282
t_anyelement = 2283
t__record = 2287
t_anynonarray = 2776
t_pg_authid = 2842
t_pg_auth_members = 2843
t__txid_snapshot = 2949
t_uuid = 2950
t__uuid = 2951
t_txid_snapshot = 2970
t_fdw_handler = 3115
t_pg_lsn = 3220
t__pg_lsn = 3221
t_tsm_handler = 3310
t_anyenum = 3500
t_tsvector = 3614
t_tsquery = 3615
t_gtsvector = 3642
t__tsvector = 3643
t__gtsvector = 3644
t__tsquery = 3645
t_regconfig = 3734
t__regconfig = 3735
t_regdictionary = 3769
t__regdictionary = 3770
t_jsonb = 3802
t__jsonb = 3807
t_anyrange = 3831
t_event_trigger = 3838
t_int4range = 3904
t__int4range = 3905
t_numrange = 3906
t__numrange = 3907
t_tsrange = 3908
t__tsrange = 3909
t_tstzrange = 3910
t__tstzrange = 3911
t_daterange = 3912
t__daterange = 3913
t_int8range = 3926
t__int8range = 3927
t_pg_shseclabel = 4066
t_regnamespace = 4089
t__regnamespace = 4090
t_regrole = 4096
t__regrole = 4097
}
+537
View File
@@ -0,0 +1,537 @@
module pg
import orm
import time
import net.conv
// ---- ORM on Conn (single pinned connection) ----
// select is used internally by V's ORM for processing `SELECT ` queries.
pub fn (c &Conn) select(config orm.SelectConfig, data orm.QueryData, where orm.QueryData) ![][]orm.Primitive {
c.ensure_active()!
where_with_tenant := orm.apply_tenant_filter(config.table, where)
query := orm.orm_select_gen(config, '"', true, '$', 1, where_with_tenant)
rows := pg_stmt_worker(c, query, where_with_tenant, data)!
mut ret := [][]orm.Primitive{}
for row in rows {
mut row_data := []orm.Primitive{}
for i, val in row.vals {
row_data << val_to_primitive(val, config.types[i])!
}
ret << row_data
}
return ret
}
// insert is used internally by V's ORM for processing `INSERT ` queries.
pub fn (c &Conn) insert(table orm.Table, data orm.QueryData) ! {
c.ensure_active()!
query, converted_data :=
orm.orm_stmt_gen(.pg, table, '"', .insert, true, '$', 1, data, orm.QueryData{})
pg_stmt_worker(c, query, converted_data, orm.QueryData{})!
}
// update is used internally by V's ORM for processing `UPDATE ` queries.
pub fn (c &Conn) update(table orm.Table, data orm.QueryData, where orm.QueryData) ! {
c.ensure_active()!
where_with_tenant := orm.apply_tenant_filter(table, where)
query, _ := orm.orm_stmt_gen(.default, table, '"', .update, true, '$', 1, data,
where_with_tenant)
pg_stmt_worker(c, query, data, where_with_tenant)!
}
// delete is used internally by V's ORM for processing `DELETE ` queries.
pub fn (c &Conn) delete(table orm.Table, where orm.QueryData) ! {
c.ensure_active()!
where_with_tenant := orm.apply_tenant_filter(table, where)
query, _ := orm.orm_stmt_gen(.default, table, '"', .delete, true, '$', 1, orm.QueryData{},
where_with_tenant)
pg_stmt_worker(c, query, orm.QueryData{}, where_with_tenant)!
}
// last_id is used internally by V's ORM for post-processing `INSERT ` queries.
pub fn (c &Conn) last_id() int {
query := 'SELECT LASTVAL();'
return c.q_int(query) or { 0 }
}
// create is used internally by V's ORM for processing table creation queries (DDL).
pub fn (c &Conn) create(table orm.Table, fields []orm.TableField) ! {
query := orm.orm_table_gen(.pg, table, '"', true, 0, fields, pg_type_from_v, false) or {
return err
}
stmts := query.split(';')
for stmt in stmts {
if stmt != '' {
c.exec(stmt + ';')!
}
}
}
// drop is used internally by V's ORM for processing table destroying queries (DDL).
pub fn (c &Conn) drop(table orm.Table) ! {
query := 'DROP TABLE "${table.name}";'
c.exec(query)!
}
// orm_begin starts a transaction on this conn.
pub fn (c &Conn) orm_begin() ! {
c.begin_on_conn()!
}
// orm_commit commits the transaction on this conn.
pub fn (c &Conn) orm_commit() ! {
c.commit()!
}
// orm_rollback rolls back the transaction on this conn.
pub fn (c &Conn) orm_rollback() ! {
c.rollback()!
}
// orm_savepoint creates a savepoint on this conn.
pub fn (c &Conn) orm_savepoint(name string) ! {
c.savepoint(name)!
}
// orm_rollback_to rolls back to a savepoint on this conn.
pub fn (c &Conn) orm_rollback_to(name string) ! {
c.rollback_to(name)!
}
// orm_release_savepoint releases a savepoint on this conn.
pub fn (c &Conn) orm_release_savepoint(name string) ! {
c.release_savepoint(name)!
}
// ---- ORM on DB (acquire-use-release per call) ----
// select acquires a conn from the pool and runs the ORM SELECT on it.
pub fn (mut db DB) select(config orm.SelectConfig, data orm.QueryData, where orm.QueryData) ![][]orm.Primitive {
mut c := db.pool.acquire()!
defer {
c.close() or {}
}
return c.select(config, data, where)
}
// insert acquires a conn from the pool, runs the ORM INSERT on it, and
// stashes LASTVAL() captured on the same session for the calling thread.
// V's `sql db { insert ... }` macro emits a follow-up `db.last_id()` call;
// stashing here lets that read return the correct id even though the pool
// may hand out a different conn for the second call.
pub fn (mut db DB) insert(table orm.Table, data orm.QueryData) ! {
mut c := db.pool.acquire()!
defer {
c.close() or {}
}
c.insert(table, data)!
db.pool.stash_last_id(c.last_id())
}
// update acquires a conn from the pool and runs the ORM UPDATE on it.
pub fn (mut db DB) update(table orm.Table, data orm.QueryData, where orm.QueryData) ! {
mut c := db.pool.acquire()!
defer {
c.close() or {}
}
c.update(table, data, where)!
}
// delete acquires a conn from the pool and runs the ORM DELETE on it.
pub fn (mut db DB) delete(table orm.Table, where orm.QueryData) ! {
mut c := db.pool.acquire()!
defer {
c.close() or {}
}
c.delete(table, where)!
}
// create acquires a conn from the pool and runs CREATE TABLE on it.
pub fn (mut db DB) create(table orm.Table, fields []orm.TableField) ! {
mut c := db.pool.acquire()!
defer {
c.close() or {}
}
c.create(table, fields)!
}
// drop acquires a conn from the pool and runs DROP TABLE on it.
pub fn (mut db DB) drop(table orm.Table) ! {
mut c := db.pool.acquire()!
defer {
c.close() or {}
}
c.drop(table)!
}
// last_id returns the id stashed by this thread's most recent `DB.insert`
// (or 0 if there is none). LASTVAL() itself is session-scoped, so calling
// it on a freshly-checked-out pool conn would return the wrong value or 0;
// `DB.insert` captures it on the same conn that ran the INSERT and stashes
// it per-thread, which is what V's ORM macro expects.
pub fn (mut db DB) last_id() int {
return db.pool.take_last_id()
}
// ---- ORM on Tx (use the pinned conn) ----
// select runs the ORM SELECT on the pinned transaction conn.
pub fn (mut tx Tx) select(config orm.SelectConfig, data orm.QueryData, where orm.QueryData) ![][]orm.Primitive {
tx.ensure_active()!
return tx.conn.select(config, data, where)
}
// insert runs the ORM INSERT on the pinned transaction conn.
pub fn (mut tx Tx) insert(table orm.Table, data orm.QueryData) ! {
tx.ensure_active()!
tx.conn.insert(table, data)!
}
// update runs the ORM UPDATE on the pinned transaction conn.
pub fn (mut tx Tx) update(table orm.Table, data orm.QueryData, where orm.QueryData) ! {
tx.ensure_active()!
tx.conn.update(table, data, where)!
}
// delete runs the ORM DELETE on the pinned transaction conn.
pub fn (mut tx Tx) delete(table orm.Table, where orm.QueryData) ! {
tx.ensure_active()!
tx.conn.delete(table, where)!
}
// create runs CREATE TABLE on the pinned transaction conn.
pub fn (mut tx Tx) create(table orm.Table, fields []orm.TableField) ! {
tx.ensure_active()!
tx.conn.create(table, fields)!
}
// drop runs DROP TABLE on the pinned transaction conn.
pub fn (mut tx Tx) drop(table orm.Table) ! {
tx.ensure_active()!
tx.conn.drop(table)!
}
// last_id returns the last inserted id on the pinned conn.
pub fn (mut tx Tx) last_id() int {
if tx.done || isnil(tx.conn) {
return 0
}
return tx.conn.last_id()
}
// orm_begin is a no-op on Tx (begin already ran when the Tx was created).
// It exists so Tx satisfies `orm.TransactionalConnection` for nested savepoints.
pub fn (mut tx Tx) orm_begin() ! {
}
// orm_commit commits the Tx.
pub fn (mut tx Tx) orm_commit() ! {
tx.commit()!
}
// orm_rollback rolls back the Tx.
pub fn (mut tx Tx) orm_rollback() ! {
tx.rollback()!
}
// orm_savepoint creates a savepoint inside the Tx.
pub fn (mut tx Tx) orm_savepoint(name string) ! {
tx.savepoint(name)!
}
// orm_rollback_to rolls back to a savepoint inside the Tx.
pub fn (mut tx Tx) orm_rollback_to(name string) ! {
tx.rollback_to(name)!
}
// orm_release_savepoint releases a savepoint inside the Tx.
pub fn (mut tx Tx) orm_release_savepoint(name string) ! {
tx.release_savepoint(name)!
}
// ---- utils ----
fn pg_stmt_binder(mut types []u32, mut vals []&char, mut lens []int, mut formats []int, d orm.QueryData) {
for data in d.data {
pg_stmt_match(mut types, mut vals, mut lens, mut formats, data)
}
}
fn pg_stmt_match_array[T](mut types []u32, mut vals []&char, mut lens []int, mut formats []int, data []T) {
for element in data {
pg_stmt_match(mut types, mut vals, mut lens, mut formats, orm.Primitive(element))
}
}
fn pg_stmt_match(mut types []u32, mut vals []&char, mut lens []int, mut formats []int, data orm.Primitive) {
match data {
bool {
types << u32(Oid.t_bool)
vals << &char(&data)
lens << int(sizeof(bool))
formats << 1
}
u8 {
types << u32(Oid.t_char)
vals << &char(&data)
lens << int(sizeof(u8))
formats << 1
}
u16 {
types << u32(Oid.t_int2)
num := conv.hton16(data)
vals << &char(&num)
lens << int(sizeof(u16))
formats << 1
}
u32 {
types << u32(Oid.t_int4)
num := conv.hton32(data)
vals << &char(&num)
lens << int(sizeof(u32))
formats << 1
}
u64 {
types << u32(Oid.t_int8)
num := conv.hton64(data)
vals << &char(&num)
lens << int(sizeof(u64))
formats << 1
}
i8 {
types << u32(Oid.t_char)
vals << &char(&data)
lens << int(sizeof(i8))
formats << 1
}
i16 {
types << u32(Oid.t_int2)
num := conv.hton16(u16(data))
vals << &char(&num)
lens << int(sizeof(i16))
formats << 1
}
int {
types << u32(Oid.t_int4)
num := conv.hton32(u32(data))
vals << &char(&num)
lens << int(sizeof(int))
formats << 1
}
i64 {
types << u32(Oid.t_int8)
num := conv.hton64(u64(data))
vals << &char(&num)
lens << int(sizeof(i64))
formats << 1
}
f32 {
types << u32(Oid.t_float4)
num := conv.htonf32(f32(data))
vals << &char(&num)
lens << int(sizeof(f32))
formats << 1
}
f64 {
types << u32(Oid.t_float8)
num := conv.htonf64(f64(data))
vals << &char(&num)
lens << int(sizeof(f64))
formats << 1
}
string {
// If paramTypes is NULL, or any particular element in the array is zero,
// the server infers a data type for the parameter symbol in the same way
// it would do for an untyped literal string.
types << u32(0)
vals << &char(data.str)
lens << data.len
formats << 0
}
time.Time {
datetime := data.format_ss()
types << u32(0)
vals << &char(datetime.str)
lens << datetime.len
formats << 0
}
orm.InfixType {
pg_stmt_match(mut types, mut vals, mut lens, mut formats, data.right)
}
orm.Null {
types << u32(0) // we do not know col type, let server infer
vals << &char(unsafe { nil }) // NULL pointer indicates NULL
lens << int(0) // ignored
formats << 0 // ignored
}
[]orm.Primitive {
pg_stmt_match_array(mut types, mut vals, mut lens, mut formats, data)
}
[]bool {
pg_stmt_match_array(mut types, mut vals, mut lens, mut formats, data)
}
[]f32 {
pg_stmt_match_array(mut types, mut vals, mut lens, mut formats, data)
}
[]f64 {
pg_stmt_match_array(mut types, mut vals, mut lens, mut formats, data)
}
[]i16 {
pg_stmt_match_array(mut types, mut vals, mut lens, mut formats, data)
}
[]i64 {
pg_stmt_match_array(mut types, mut vals, mut lens, mut formats, data)
}
[]i8 {
pg_stmt_match_array(mut types, mut vals, mut lens, mut formats, data)
}
[]int {
pg_stmt_match_array(mut types, mut vals, mut lens, mut formats, data)
}
[]string {
pg_stmt_match_array(mut types, mut vals, mut lens, mut formats, data)
}
[]time.Time {
pg_stmt_match_array(mut types, mut vals, mut lens, mut formats, data)
}
[]u16 {
pg_stmt_match_array(mut types, mut vals, mut lens, mut formats, data)
}
[]u32 {
pg_stmt_match_array(mut types, mut vals, mut lens, mut formats, data)
}
[]u64 {
pg_stmt_match_array(mut types, mut vals, mut lens, mut formats, data)
}
[]u8 {
pg_stmt_match_array(mut types, mut vals, mut lens, mut formats, data)
}
[]orm.InfixType {
pg_stmt_match_array(mut types, mut vals, mut lens, mut formats, data)
}
}
}
fn pg_type_from_v(typ int) !string {
str := match typ {
orm.type_idx['i8'], orm.type_idx['i16'], orm.type_idx['u8'], orm.type_idx['u16'] {
'SMALLINT'
}
orm.type_idx['bool'] {
'BOOLEAN'
}
orm.type_idx['int'], orm.type_idx['u32'] {
'INT'
}
orm.time_ {
'TIMESTAMP'
}
orm.enum_ {
'BIGINT'
}
orm.type_idx['i64'], orm.type_idx['u64'] {
'BIGINT'
}
orm.float[0] {
'REAL'
}
orm.float[1] {
'DOUBLE PRECISION'
}
orm.type_string {
'TEXT'
}
orm.serial {
'SERIAL'
}
else {
''
}
}
if str == '' {
return error('Unknown type ${typ}')
}
return str
}
fn val_to_primitive(val ?string, typ int) !orm.Primitive {
if str := val {
match typ {
// bool
orm.type_idx['bool'] {
return orm.Primitive(str == 't')
}
// i8
orm.type_idx['i8'] {
return orm.Primitive(str.i8())
}
// i16
orm.type_idx['i16'] {
return orm.Primitive(str.i16())
}
// int
orm.type_idx['int'] {
return orm.Primitive(str.int())
}
// i64
orm.type_idx['i64'] {
return orm.Primitive(str.i64())
}
// u8
orm.type_idx['u8'] {
data := str.i8()
return orm.Primitive(*unsafe { &u8(&data) })
}
// u16
orm.type_idx['u16'] {
data := str.i16()
return orm.Primitive(*unsafe { &u16(&data) })
}
// u32
orm.type_idx['u32'] {
data := str.int()
return orm.Primitive(*unsafe { &u32(&data) })
}
// u64
orm.type_idx['u64'] {
data := str.i64()
return orm.Primitive(*unsafe { &u64(&data) })
}
// f32
orm.type_idx['f32'] {
return orm.Primitive(str.f32())
}
// f64
orm.type_idx['f64'] {
return orm.Primitive(str.f64())
}
orm.type_string {
return orm.Primitive(str)
}
orm.time_ {
if str.contains_any(' /:-') {
date_time_str := time.parse(str)!
return orm.Primitive(date_time_str)
}
timestamp := str.int()
return orm.Primitive(time.unix(timestamp))
}
orm.enum_ {
return orm.Primitive(str.i64())
}
else {}
}
return error('Unknown field type ${typ}')
} else {
return orm.Null{}
}
}
File diff suppressed because it is too large. Load diff
+45
View File
@@ -0,0 +1,45 @@
// vtest build: started_postgres?
module main
import db.pg
@[table: 'demo']
struct Demo {
id int @[primary; sql: serial]
number f64
number2 f32
}
fn test_float_field() {
$if !network ? {
eprintln('> Skipping test ${@FN}, since `-d network` is not passed.')
eprintln('> This test requires a working postgres server running on localhost.')
return
}
conn := 'host=localhost user=postgres password=12345678' // insert own connection string
mut db := pg.connect_with_conninfo(conn)!
defer {
db.close() or {}
}
sql db {
create table Demo
}!
demo := Demo{0, 9.58815, 9.58815}
sql db {
insert demo into Demo
}!
rows := sql db {
select from Demo
}!
assert rows[0].number == 9.58815
assert rows[0].number2 == 9.58815
sql db {
drop table Demo
}!
println(rows)
}
+332
View File
@@ -0,0 +1,332 @@
// vtest build: started_postgres?
module main
import orm
import db.pg
import time
struct TestCustomSqlType {
id int @[primary; sql: serial]
custom string @[sql_type: 'TEXT']
custom1 string @[sql_type: 'VARCHAR(191)']
custom2 string @[sql_type: 'TIMESTAMP']
custom3 string @[sql_type: 'uuid']
}
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: 'TIMESTAMP']
updated_at string @[sql_type: 'TIMESTAMP']
deleted_at time.Time
}
struct TestDefaultAttribute {
id string @[default: 'gen_random_uuid()'; primary; sql_type: 'uuid']
name string
created_at string @[default: 'CURRENT_TIMESTAMP'; sql_type: 'TIMESTAMP']
}
struct TestInsertDefaultValues {
id int @[primary; sql: serial]
example string @[default: '']
}
@[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_pg_orm() {
$if !network ? {
eprintln('> Skipping test ${@FN}, since `-d network` is not passed.')
eprintln('> This test requires a working postgres server running on localhost.')
return
}
mut db := pg.connect(
host: 'localhost'
user: 'postgres'
password: '12345678'
dbname: 'postgres'
) or { panic(err) }
defer {
db.close() or {}
}
db.exec('create extension if not exists pgcrypto') or { panic(err) }
table := orm.Table{
name: 'Test'
}
db.drop(table) or {}
db.create(table, [
orm.TableField{
name: 'id'
typ: typeof[string]().idx
// is_time: false
default_val: ''
is_arr: false
attrs: [
VAttribute{
name: 'primary'
has_arg: false
arg: ''
kind: .plain
},
VAttribute{
name: 'sql'
has_arg: true
arg: 'serial'
kind: .plain
},
]
},
orm.TableField{
name: 'name'
typ: typeof[string]().idx
// is_time: false
default_val: ''
is_arr: false
attrs: []
},
orm.TableField{
name: 'age'
typ: typeof[i64]().idx
// is_time: false
default_val: ''
is_arr: false
attrs: []
},
]) 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
aggregate_kind: .none
has_where: true
has_order: false
order: ''
order_type: .asc
has_limit: false
primary: 'id'
has_offset: false
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'), orm.Primitive(101)]
types: []
kinds: [.eq, .eq]
is_and: [true]
}) 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
}
/** test orm sql type
* - verify if all type create by attribute sql_type has created
*/
sql db {
drop table TestCustomSqlType
} or {}
sql db {
create table TestCustomSqlType
}!
mut result_custom_sql := db.exec("
SELECT DATA_TYPE
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'testcustomsqltype'
ORDER BY ORDINAL_POSITION
") or {
println(err)
panic(err)
}
mut information_schema_data_types_results := []string{}
information_schema_custom_sql := ['integer', 'text', 'character varying',
'timestamp without time zone', 'uuid']
for data_type in result_custom_sql {
x := data_type.vals[0]
information_schema_data_types_results << x?
}
assert information_schema_data_types_results == 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)
}
model := TestTimeType{
username: 'hitalo'
created_at: today
updated_at: today.str()
deleted_at: today
}
sql db {
create table TestTimeType
}!
sql db {
insert model into TestTimeType
}!
results := sql db {
select from TestTimeType where username == 'hitalo'
}!
sql db {
drop table TestTimeType
}!
assert results[0].username == model.username
assert results[0].created_at == model.created_at
assert results[0].updated_at == model.updated_at
assert results[0].deleted_at == model.deleted_at
/** test default attribute
*/
sql db {
create table TestDefaultAttribute
}!
mut result_defaults := db.exec("
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{}
for defaults in result_defaults {
x := defaults.vals[0]
information_schema_defaults_results << x or { '' }
}
sql db {
drop table TestDefaultAttribute
}!
assert ['gen_random_uuid()', '', 'CURRENT_TIMESTAMP'] == information_schema_defaults_results
/** test inserting only default values
*/
sql db {
create table TestInsertDefaultValues
}!
model_default_values := TestInsertDefaultValues{
example: ''
}
sql db {
insert model_default_values into TestInsertDefaultValues
}!
inserted_default_values := sql db {
select from TestInsertDefaultValues
}!
sql db {
drop table TestInsertDefaultValues
}!
assert inserted_default_values.len == 1
assert inserted_default_values[0].example == ''
/** test comment attribute
*/
sql db {
create table TestCommentAttribute
}!
mut column_comments := db.exec("
SELECT
a.attname AS column_name,
col_description(a.attrelid, a.attnum) AS column_comment
FROM pg_attribute a
JOIN pg_class c ON c.oid = a.attrelid
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relname = 'testcommentattribute'
AND n.nspname = 'public'
AND a.attnum > 0
AND NOT a.attisdropped
ORDER BY a.attnum
") or {
println(err)
panic(err)
}
mut table_comment := db.exec("
SELECT
nspname AS schema_name,
relname AS table_name,
obj_description(pc.oid) AS table_comment
FROM pg_class pc
JOIN pg_namespace pn ON pn.oid = pc.relnamespace
WHERE pc.relkind = 'r' AND pc.relname = 'testcommentattribute'
ORDER BY schema_name, table_name
") or {
println(err)
panic(err)
}
sql db {
drop table TestCommentAttribute
}!
mut information_schema_column_comment_results := []string{}
for comment in column_comments {
x := comment.vals[1]
information_schema_column_comment_results << x or { '' }
}
assert information_schema_column_comment_results == ['', 'real user name', '']
mut information_schema_table_comment_result := []string{}
for comment in table_comment {
x := comment.vals[2]
information_schema_table_comment_result << x or { '' }
}
assert information_schema_table_comment_result == ['This is a table comment']
}
+57
View File
@@ -0,0 +1,57 @@
// vtest build: started_postgres?
module main
import db.pg
struct Info {
table_schema string
relname string
attname string
typename string
typealign string
typlen int
}
fn deref(val ?string) string {
return val or { panic('no value') }
}
fn row_mapper(res pg.Result, row pg.Row) !Info {
return Info{
table_schema: deref(row.vals[res.cols['table_schema']])
relname: deref(row.vals[res.cols['relname']])
attname: deref(row.vals[res.cols['attname']])
typename: deref(row.vals[res.cols['typename']])
typealign: deref(row.vals[res.cols['typealign']])
typlen: deref(row.vals[res.cols['typlen']]).int()
}
}
fn test_large_exec() {
$if !network ? {
eprintln('> Skipping test ${@FN}, since `-d network` is not passed.')
eprintln('> This test requires a working postgres server running on localhost.')
return
}
mut db := pg.connect(pg.Config{ user: 'postgres', password: '12345678', dbname: 'postgres' })!
defer {
db.close() or {}
}
result := db.exec_result('
SELECT ischema.table_schema, c.relname, a.attname, t.typname, t.typalign, t.typlen
FROM pg_class c
JOIN information_schema.tables ischema on ischema.table_name = c.relname
JOIN pg_attribute a ON (a.attrelid = c.oid)
JOIN pg_type t ON (t.oid = a.atttypid)
WHERE
a.attnum >= 0
')!
infos := result.as_structs(row_mapper)!
assert result.rows.len > 0 && infos.len == result.rows.len
// println(infos)
}
+164
View File
@@ -0,0 +1,164 @@
// vtest build: started_postgres?
module main
import db.pg
import orm
fn test_large_exec() {
$if !network ? {
eprintln('> Skipping test ${@FN}, since `-d network` is not passed.')
eprintln('> This test requires a working postgres server running on localhost.')
return
}
mut db := pg.connect(pg.Config{ user: 'postgres', password: '12345678', dbname: 'postgres' })!
defer {
db.close() or {}
}
assert db.validate()!
rows := db.exec('
SELECT ischema.table_schema, c.relname, a.attname, t.typname, t.typalign, t.typlen
FROM pg_class c
JOIN information_schema.tables ischema on ischema.table_name = c.relname
JOIN pg_attribute a ON (a.attrelid = c.oid)
JOIN pg_type t ON (t.oid = a.atttypid)
WHERE
a.attnum >= 0
')!
for row in rows {
// We just need to access the memory to ensure it's properly allocated
row.str()
}
}
fn test_prepared() {
$if !network ? {
eprintln('> Skipping test ${@FN}, since `-d network` is not passed.')
eprintln('> This test requires a working postgres server running on localhost.')
return
}
mut db := pg.connect(pg.Config{ user: 'postgres', password: '12345678', dbname: 'postgres' })!
defer {
db.close() or {}
}
// Prepared statements are session-scoped, so pin a single conn.
mut c := db.conn()!
defer {
c.close() or {}
}
c.prepare('test_prepared', 'SELECT NOW(), $1 AS NAME', 1) or { panic(err) }
result := c.exec_prepared('test_prepared', ['hello world']) or { panic(err) }
assert result.len == 1
}
fn test_transaction() {
$if !network ? {
eprintln('> Skipping test ${@FN}, since `-d network` is not passed.')
eprintln('> This test requires a working postgres server running on localhost.')
return
}
mut db := pg.connect(pg.Config{ user: 'postgres', password: '12345678', dbname: 'postgres' })!
defer {
db.close() or {}
}
db.exec('drop table if exists users')!
db.exec('create table if not exists users (
id SERIAL PRIMARY KEY,
username TEXT,
last_name TEXT NULL DEFAULT NULL
)')!
// orm.TransactionalConnection requires a pinned conn (DB is pool-backed
// and cannot guarantee BEGIN/COMMIT land on the same physical conn).
mut c := db.conn()!
mut tc := orm.TransactionalConnection(c)
mut otx := orm.begin(mut tc)!
otx.transaction[int](fn (mut tx orm.Tx) !int {
return 1
})!
otx.commit()!
c.close()!
mut tx := db.begin()!
tx.exec("insert into users (username) values ('jackson')")!
tx.savepoint('savepoint1')!
tx.exec("insert into users (username) values ('kitty')")!
tx.rollback_to('savepoint1')!
tx.exec("insert into users (username) values ('mars')")!
tx.commit()!
rows := db.exec('select * from users')!
for row in rows {
// We just need to access the memory to ensure it's properly allocated
dump(row.str())
}
}
fn test_listen_notify() {
$if !network ? {
eprintln('> Skipping test ${@FN}, since `-d network` is not passed.')
eprintln('> This test requires a working postgres server running on localhost.')
return
}
mut db := pg.connect(pg.Config{ user: 'postgres', password: '12345678', dbname: 'postgres' })!
defer {
db.close() or {}
}
// LISTEN/NOTIFY is session-scoped; pin a single conn for the test.
mut c := db.conn()!
defer {
c.close() or {}
}
// Test listen
c.listen('test_channel')!
// Test notify with payload
c.notify('test_channel', 'hello world')!
// Consume input to process the notification
c.consume_input()!
// Get the notification
if notification := c.get_notification() {
assert notification.channel == 'test_channel'
assert notification.payload == 'hello world'
assert notification.pid > 0
} else {
assert false, 'Expected a notification but got none'
}
// Test notify without payload
c.notify('test_channel', '')!
c.consume_input()!
if notification := c.get_notification() {
assert notification.channel == 'test_channel'
assert notification.payload == ''
} else {
assert false, 'Expected a notification but got none'
}
// Test that no more notifications are pending
assert c.get_notification() == none
// Test unlisten
c.unlisten('test_channel')!
// Test unlisten_all
c.listen('channel1')!
c.listen('channel2')!
c.unlisten_all()!
// Test socket (should return valid fd)
socket_fd := c.socket()
assert socket_fd >= 0
}
+375
View File
@@ -0,0 +1,375 @@
module pg
import sync
import time
// PoolStats reports the current state of a `DB`'s connection pool.
pub struct PoolStats {
pub:
max_open_connections int // configured limit (0 = unlimited)
open_connections int // total in-use + idle conns
in_use int // conns currently checked out
idle int // conns parked as idle
wait_count int // number of callers currently blocked on acquire
}
// PoolConfig configures pool behavior at construction time.
@[params]
pub struct PoolConfig {
pub:
max_open_conns int // 0 = unlimited
max_idle_conns int = 2 // 0 = keep no idle conns
conn_max_lifetime time.Duration // 0 = unlimited
}
// IdleSlot is the pool's internal representation of a pooled libpq handle.
// The pool tracks raw PGconn* metadata in idle/waiter channels; `acquire`
// allocates a fresh `&Conn` wrapping the slot and `release` extracts the
// metadata back. Keeping the wrapper separate from the slot means a stale
// `&Conn` kept by user code after `close()` cannot be revived even when
// the pool re-hands the same physical PGconn* to another caller.
struct IdleSlot {
handle voidptr
created_at time.Time
mut:
bad bool
}
@[heap]
struct Pool {
mut:
mu &sync.Mutex = unsafe { nil }
conninfo string
max_open int
max_idle int
max_lifetime time.Duration
idle []IdleSlot
open_count int
waiters []chan IdleSlot
closed bool
// last_ids stores the per-thread last-inserted id so DB.last_id() returns
// the value captured on the same pooled conn that ran the INSERT, instead
// of calling LASTVAL() on whatever conn the pool happens to hand out next
// (which is session-scoped and would return 0 or a wrong value).
last_ids_mu &sync.Mutex = unsafe { nil }
last_ids map[u64]i64
}
fn new_pool(conninfo string, cfg PoolConfig) &Pool {
mut p := &Pool{
mu: sync.new_mutex()
last_ids_mu: sync.new_mutex()
conninfo: conninfo
max_open: cfg.max_open_conns
max_idle: cfg.max_idle_conns
max_lifetime: cfg.conn_max_lifetime
}
if p.max_open < 0 {
p.max_open = 0
}
if p.max_idle < 0 {
p.max_idle = 0
}
return p
}
// wrap_slot builds the fresh `&Conn` that gets handed to a caller. Each
// acquire allocates a new wrapper so a previously-checked-out (and since
// released) `&Conn` reference can never be reused to reach the underlying
// PGconn*.
fn (p &Pool) wrap_slot(slot IdleSlot) &Conn {
return &Conn{
conn: slot.handle
pool: unsafe { p }
created_at: slot.created_at
bad: slot.bad
}
}
fn (mut p Pool) acquire() !&Conn {
for {
p.mu.lock()
if p.closed {
p.mu.unlock()
return error('pg: pool is closed')
}
// Pop newest idle slot (LIFO keeps the freshest connection warm)
for p.idle.len > 0 {
slot := p.idle.last()
p.idle.delete_last()
if slot_expired(slot, p.max_lifetime) || slot_bad(slot) {
physical_close_handle(slot.handle)
p.open_count--
continue
}
p.mu.unlock()
return p.wrap_slot(slot)
}
// Capacity available: open a new conn outside the lock
if p.max_open == 0 || p.open_count < p.max_open {
p.open_count++
conninfo := p.conninfo
p.mu.unlock()
slot := connect_slot(conninfo) or {
p.mu.lock()
p.open_count--
p.mu.unlock()
return err
}
// close() may have run while we were dialing outside the lock.
// Honor the close contract by tearing the fresh handle down
// instead of returning a live Conn after shutdown.
p.mu.lock()
if p.closed {
p.open_count--
p.mu.unlock()
physical_close_handle(slot.handle)
return error('pg: pool is closed')
}
p.mu.unlock()
return p.wrap_slot(slot)
}
// At capacity: wait for a release/close/cap-raise to signal us.
// Senders transfer slot ownership via this channel; a sentinel slot
// with a nil handle means "capacity changed, retry the acquire
// loop" (used by set_max_open when raising the limit).
waiter := chan IdleSlot{cap: 1}
p.waiters << waiter
p.mu.unlock()
slot := <-waiter or { return error('pg: pool was closed while waiting for connection') }
if isnil(slot.handle) {
continue
}
return p.wrap_slot(slot)
}
return error('pg: unreachable')
}
fn (mut p Pool) release(conn &Conn) {
if isnil(conn) {
return
}
mut c := unsafe { conn }
// Detach the wrapper from the physical handle before doing anything else.
// This is what makes a stale `&Conn` reference held by user code inert:
// any subsequent method call on it will see `c.conn == nil` and error.
// It is also the idempotency guard against double-close — a second
// release() finds nothing to return to the pool and is a no-op.
if isnil(c.conn) {
return
}
slot := IdleSlot{
handle: c.conn
created_at: c.created_at
bad: c.bad
}
c.conn = unsafe { nil }
c.pool = unsafe { nil }
p.mu.lock()
if p.closed {
// close() only decremented open_count for slots it found parked;
// in-use conns are accounted for here as they trickle back.
p.open_count--
p.mu.unlock()
physical_close_handle(slot.handle)
return
}
// Discard broken or expired conns
if slot_bad(slot) || slot_expired(slot, p.max_lifetime) {
physical_close_handle(slot.handle)
p.open_count--
// A capacity slot just opened; hand it to a waiter as a fresh conn
if p.waiters.len > 0 && (p.max_open == 0 || p.open_count < p.max_open) {
waiter := p.waiters[0]
p.waiters.delete(0)
p.open_count++
conninfo := p.conninfo
p.mu.unlock()
new_slot := connect_slot(conninfo) or {
p.mu.lock()
p.open_count--
// Capacity is now open but the dial just failed. Wake every
// other parked waiter too, otherwise they hang forever waiting
// for a release that will never come (e.g. max_open=1).
extras := p.waiters.clone()
p.waiters = []chan IdleSlot{}
p.mu.unlock()
waiter.close()
for w in extras {
w.close()
}
return
}
p.mu.lock()
if p.closed {
// Pool closed during the dial: drop the new conn and signal the waiter.
p.open_count--
p.mu.unlock()
physical_close_handle(new_slot.handle)
waiter.close()
return
}
waiter <- new_slot
p.mu.unlock()
return
}
p.mu.unlock()
return
}
// If a recent set_max_open() shrank the cap below open_count, this
// returning slot has to be retired even when a waiter is queued —
// otherwise the fast hand-off keeps open_count pinned above the new
// limit forever under steady traffic. Wake the waiter with a retry
// sentinel so it re-evaluates capacity under the new cap.
if p.max_open > 0 && p.open_count > p.max_open {
physical_close_handle(slot.handle)
p.open_count--
if p.waiters.len > 0 {
waiter := p.waiters[0]
p.waiters.delete(0)
waiter <- IdleSlot{
handle: unsafe { nil }
}
}
p.mu.unlock()
return
}
// Healthy conn: prefer handing it directly to a waiter. Send under the
// lock (cap:1 makes it non-blocking) so close() can't slip in and orphan
// the popped waiter with a live conn.
if p.waiters.len > 0 {
waiter := p.waiters[0]
p.waiters.delete(0)
waiter <- slot
p.mu.unlock()
return
}
// Park as idle, unless we'd exceed max_idle (0 = keep no idle conns)
// or we are over a recently-shrunk max_open and need to converge.
if p.idle.len >= p.max_idle || (p.max_open > 0 && p.open_count > p.max_open) {
physical_close_handle(slot.handle)
p.open_count--
p.mu.unlock()
return
}
p.idle << slot
p.mu.unlock()
}
fn (mut p Pool) close() {
p.mu.lock()
if p.closed {
p.mu.unlock()
return
}
p.closed = true
for slot in p.idle {
physical_close_handle(slot.handle)
}
idle_len := p.idle.len
p.idle = []IdleSlot{}
p.open_count -= idle_len
for waiter in p.waiters {
waiter.close()
}
p.waiters = []chan IdleSlot{}
p.mu.unlock()
}
fn (mut p Pool) stats() PoolStats {
p.mu.lock()
stats := PoolStats{
max_open_connections: p.max_open
open_connections: p.open_count
idle: p.idle.len
in_use: p.open_count - p.idle.len
wait_count: p.waiters.len
}
p.mu.unlock()
return stats
}
fn (mut p Pool) set_max_open(n int) {
mut nn := n
if nn < 0 {
nn = 0
}
p.mu.lock()
p.max_open = nn
// Shrink: drop idle slots until we are back under the new cap. Without
// this, acquire() pops idle slots before checking max_open, so two
// callers could still both succeed after set_max_open_conns(1) when two
// warm conns are parked. In-use conns can't be reclaimed here — they get
// discarded on release once open_count is over the cap.
if nn > 0 {
for p.idle.len > 0 && p.open_count > nn {
slot := p.idle.last()
p.idle.delete_last()
physical_close_handle(slot.handle)
p.open_count--
}
}
// Raise: wake parked waiters so they can retry the acquire loop and
// dial against the new headroom. Without this nudge they stay blocked
// until some other release happens, which may never come if all current
// conns are long-lived.
if p.waiters.len > 0 && (nn == 0 || p.open_count < nn) {
spare := if nn == 0 { p.waiters.len } else { nn - p.open_count }
mut n_wake := if spare < p.waiters.len { spare } else { p.waiters.len }
for n_wake > 0 {
waiter := p.waiters[0]
p.waiters.delete(0)
waiter <- IdleSlot{
handle: unsafe { nil }
}
n_wake--
}
}
p.mu.unlock()
}
fn (mut p Pool) set_max_idle(n int) {
mut nn := n
if nn < 0 {
nn = 0
}
p.mu.lock()
p.max_idle = nn
for p.idle.len > nn {
slot := p.idle.last()
p.idle.delete_last()
physical_close_handle(slot.handle)
p.open_count--
}
p.mu.unlock()
}
fn (mut p Pool) set_conn_max_lifetime(d time.Duration) {
p.mu.lock()
p.max_lifetime = d
p.mu.unlock()
}
// stash_last_id records `id` as the last-inserted id for the current thread.
// Called by `DB.insert` right after running INSERT on the pinned conn, so the
// next `DB.last_id()` call on the same thread returns this exact value rather
// than running LASTVAL() on a different pooled session.
fn (mut p Pool) stash_last_id(id int) {
tid := sync.thread_id()
p.last_ids_mu.lock()
p.last_ids[tid] = i64(id)
p.last_ids_mu.unlock()
}
// take_last_id returns the last-inserted id stashed by this thread's most
// recent `DB.insert`, or 0 if there is none. Consuming on read keeps the
// map bounded as threads come and go, and prevents a future thread that
// reuses this tid from observing a stale id before doing its own insert.
fn (mut p Pool) take_last_id() int {
tid := sync.thread_id()
p.last_ids_mu.lock()
id := p.last_ids[tid] or { i64(0) }
p.last_ids.delete(tid)
p.last_ids_mu.unlock()
return int(id)
}
+167
View File
@@ -0,0 +1,167 @@
module pg
import io
// Tx is a database transaction. It pins a single `Conn` from the pool for
// the lifetime of the transaction, so all queries run on the same physical
// connection. The pinned conn is returned to the pool when `commit()` or
// `rollback()` is called; failing to call either leaks the conn.
@[heap]
pub struct Tx {
mut:
conn &Conn = unsafe { nil }
done bool
}
fn (tx &Tx) ensure_active() ! {
if tx.done {
return error('pg: transaction is already finished')
}
if isnil(tx.conn) {
return error('pg: transaction has no connection')
}
}
fn (mut tx Tx) finish() {
if tx.done {
return
}
tx.done = true
if !isnil(tx.conn) {
mut c := tx.conn
c.close() or {}
tx.conn = unsafe { nil }
}
}
// commit commits the transaction and returns the pinned conn to the pool.
pub fn (mut tx Tx) commit() ! {
tx.ensure_active()!
defer {
tx.finish()
}
tx.conn.commit()!
}
// rollback rolls back the transaction and returns the pinned conn to the pool.
pub fn (mut tx Tx) rollback() ! {
tx.ensure_active()!
defer {
tx.finish()
}
tx.conn.rollback()!
}
// savepoint creates a savepoint named `name`.
pub fn (mut tx Tx) savepoint(name string) ! {
tx.ensure_active()!
tx.conn.savepoint(name)!
}
// rollback_to rolls the transaction back to the savepoint named `name`.
pub fn (mut tx Tx) rollback_to(name string) ! {
tx.ensure_active()!
tx.conn.rollback_to(name)!
}
// release_savepoint releases the savepoint named `name`.
pub fn (mut tx Tx) release_savepoint(name string) ! {
tx.ensure_active()!
tx.conn.release_savepoint(name)!
}
// raw returns the pinned conn for advanced use cases. The caller MUST NOT
// call `close()` on it; the conn is owned by the transaction.
pub fn (mut tx Tx) raw() !&Conn {
tx.ensure_active()!
return tx.conn
}
// ---- exec/query helpers ----
// exec runs `query` on the pinned conn.
pub fn (mut tx Tx) exec(query string) ![]Row {
tx.ensure_active()!
return tx.conn.exec(query)
}
// exec_no_null runs `query` and returns rows with no nullable fields.
pub fn (mut tx Tx) exec_no_null(query string) ![]RowNoNull {
tx.ensure_active()!
return tx.conn.exec_no_null(query)
}
// exec_result runs `query` and returns a `Result`.
pub fn (mut tx Tx) exec_result(query string) !Result {
tx.ensure_active()!
return tx.conn.exec_result(query)
}
// exec_one runs `query` and returns its first row.
pub fn (mut tx Tx) exec_one(query string) !Row {
tx.ensure_active()!
return tx.conn.exec_one(query)
}
// exec_param_many runs `query` with the given parameters.
pub fn (mut tx Tx) exec_param_many(query string, params []string) ![]Row {
tx.ensure_active()!
return tx.conn.exec_param_many(query, params)
}
// exec_param_many_result runs `query` with parameters and returns a `Result`.
pub fn (mut tx Tx) exec_param_many_result(query string, params []string) !Result {
tx.ensure_active()!
return tx.conn.exec_param_many_result(query, params)
}
// exec_param runs `query` with one `$1` parameter.
pub fn (mut tx Tx) exec_param(query string, param string) ![]Row {
return tx.exec_param_many(query, [param])
}
// exec_param2 runs `query` with two parameters.
pub fn (mut tx Tx) exec_param2(query string, param string, param2 string) ![]Row {
return tx.exec_param_many(query, [param, param2])
}
// q_int runs `query` and returns the first column of the first row as int.
pub fn (mut tx Tx) q_int(query string) !int {
tx.ensure_active()!
return tx.conn.q_int(query)
}
// q_string runs `query` and returns the first column of the first row as string.
pub fn (mut tx Tx) q_string(query string) !string {
tx.ensure_active()!
return tx.conn.q_string(query)
}
// q_strings runs `query` and returns the full row set (alias of `exec`).
pub fn (mut tx Tx) q_strings(query string) ![]Row {
return tx.exec(query)
}
// prepare registers a prepared statement on the pinned conn.
pub fn (mut tx Tx) prepare(name string, query string, num_params int) ! {
tx.ensure_active()!
return tx.conn.prepare(name, query, num_params)
}
// exec_prepared runs a previously-prepared statement on the pinned conn.
pub fn (mut tx Tx) exec_prepared(name string, params []string) ![]Row {
tx.ensure_active()!
return tx.conn.exec_prepared(name, params)
}
// exec_prepared_result runs a previously-prepared statement on the pinned conn.
pub fn (mut tx Tx) exec_prepared_result(name string, params []string) !Result {
tx.ensure_active()!
return tx.conn.exec_prepared_result(name, params)
}
// copy_expert runs a COPY command on the pinned conn.
pub fn (mut tx Tx) copy_expert(query string, mut file io.ReaderWriter) !int {
tx.ensure_active()!
return tx.conn.copy_expert(query, mut file)
}
@@ -0,0 +1,51 @@
// vtest build: !windows
module main
import db.pg
import db.sqlite
fn test_pg_connection_user_aliases() {
assert pg.Config{
user: 'alice'
}.connection_user()! == 'alice'
assert pg.Config{
username: 'alice'
}.connection_user()! == 'alice'
assert pg.Config{
user: 'alice'
username: 'alice'
}.connection_user()! == 'alice'
if _ := pg.Config{
user: 'alice'
username: 'bob'
}.connection_user()
{
assert false
} else {
assert err.msg().contains('must match')
}
}
fn test_pg_row_value_helpers() {
mut vals := []?string{}
vals << none
vals << 'hello'
vals << ''
row := pg.Row{
vals: vals
}
assert row.val(0) == ''
assert row.val(1) == 'hello'
assert row.values() == ['', 'hello', '']
assert row.val_opt(0) == none
assert row.val_opt(1) or { '' } == 'hello'
}
fn test_sqlite_row_value_helpers() {
sqlite_row := sqlite.Row{
vals: ['hello', '']
names: ['first', 'second']
}
assert sqlite_row.val(0) == 'hello'
assert sqlite_row.values() == ['hello', '']
}
+152
View File
@@ -0,0 +1,152 @@
# Redis Client for V
This module provides a Redis client implementation in V that supports the
Redis Serialization Protocol (RESP) versions 2 (RESP2) and 3 (RESP3) with
a type-safe interface for common Redis commands.
## Features
- **Type-Safe Commands**: Auto-detects value types at compile time
- **Pipeline Support**: Group commands for batch execution
- **Connection Pooling**: Efficient resource management
- **RESP Protocol Support**: Full Redis Serialization Protocol implementation
- **Memory Efficient**: Pre-allocated buffers for minimal allocations
## Quick Start
```v
module main
import db.redis
fn main() {
// Connect to Redis
// Uncomment passwod line if authentication is needed
mut db := redis.connect(redis.Config{
// Available Config options (none of which need to be specified if you want the defauilts):
// host: 'localhost' - default
// password: 'your_password' - no default, you need to supply password if your redis server
// is set up to need one
// port: 6379 - default
// tls: false - default, set to true for ssl connection
})!
println('Server supports RESP${db.version} protocol')
// Set and get values
db.set('name', 'Alice')!
name := db.get[string]('name')!
println('Name: ${name}') // Output: Name: Alice
// Integer operations
db.set('counter', 42)!
db.incr('counter')!
counter := db.get[int]('counter')!
println('Counter: ${counter}') // Output: Counter: 43
// Clean up
db.close()!
}
```
## Supported Commands
### Key Operations
```v ignore
// Set value
db.set('key', 'value')!
db.set('number', 42)!
db.set('binary', []u8{len: 4, init: 0})!
// Get value
str_value := db.get[string]('key')!
int_value := db.get[int]('number')!
bin_value := db.get[[]u8]('binary')!
// Delete key
db.del('key')!
// Set expiration
db.expire('key', 60)! // 60 seconds
```
### Hash Operations
```v ignore
// Set hash fields
db.hset('user:1', {
'name': 'Bob',
'age': '30',
})!
// Get single field
name := db.hget[string]('user:1', 'name')!
// Get all fields
user_data := db.hgetall[string]('user:1')!
println(user_data) // Output: {'name': 'Bob', 'age': '30'}
```
### Pipeline Operations
```v ignore
// Start pipeline
db.pipeline_start()
// Queue commands
db.incr('counter')!
db.set('name', 'Charlie')!
db.get[string]('name')!
// Execute and get responses
responses := db.pipeline_execute()!
for resp in responses {
println(resp)
}
```
### Custom Commands
```v ignore
// Run raw commands
resp := db.cmd('SET', 'custom', 'value')!
result := db.cmd('GET', 'custom')!
// Complex commands
db.cmd('HSET', 'user:2', 'field1', 'value1', 'field2', '42')!
```
## Error Handling
All functions return `!` types and will return detailed errors for:
- Connection issues
- Protocol violations
- Type mismatches
- Redis error responses
- Timeout conditions
```v ignore
result := db.get[string]('nonexistent') or {
println('Key not found')
return
}
```
## Connection Management
```v ignore
config := redis.Config{
host: 'redis.server'
port: 6379
}
mut db := redis.connect(config)!
defer {
db.close() or { eprintln('Error closing connection: ${err}') }
}
```
`redis.DB` also implements `pool.ConnectionPoolable`, so it can be used
directly with `vlib/pool` connection pools.
## Performance Tips
1. **Reuse Connections**: Maintain connections instead of creating new ones
2. **Use Pipelines**: Batch commands for high-throughput operations
3. **Prefer Integers**: Use numeric types for counters and metrics
4. **Specify Types**: Always specify return types for get operations
File diff suppressed because it is too large. Load diff
+592
View File
@@ -0,0 +1,592 @@
// vtest build: started_redis?
import db.redis
import os
import rand
import time
const redis_password = os.getenv('VREDIS_PASSWORD')
// Keys and channel names used by the tests
const k_int = 'k:int'
const k_string = 'k:string'
const k_counter = 'k:counter'
const r_k_map = 'r:k:map'
const r_k_set = 'r:k:set'
const r_k_pubch = 'r:k:pubch'
// Helper publisher used by the pub/sub test. This helper swallows errors and logs
// them locally so it can be spawned safely without propagating results.
fn publish_after_delay() {
// small delay to ensure subscriber is ready
time.sleep(50 * time.millisecond)
mut conn := redis.connect() or {
eprintln('publish_after_delay: connect failed: ${err}')
return
}
defer { conn.close() or { eprintln('publish_after_delay: close failed: ${err}') } }
conn.cmd('PUBLISH', r_k_pubch, 'hello') or {
eprintln('publish_after_delay: publish failed: ${err}')
}
}
// -------------------- FAST / BASIC TESTS --------------------
// Basic functionality smoke test (fast)
fn test_redis_basic() {
mut db := redis.connect(password: redis_password)!
defer { db.close() or {} }
// Basic health check
assert db.ping()! == 'PONG'
// delete all keys first
db.cmd('FLUSHALL')!
// test set[T]
assert db.set('int', 123)! == 'OK'
assert db.set('string', 'abc')! == 'OK'
assert db.set('bin', [u8(0x00), 0x01, 0x02, 0x03])! == 'OK'
// test get[T]
assert db.get[i64]('int')! == 123
assert db.get[string]('string')! == 'abc'
assert db.get[[]u8]('bin')! == [u8(0x00), 0x01, 0x02, 0x03]
// test incr/decr
assert db.incr('int')! == 124
assert db.decr('int')! == 123
// test hset/hget/hgetall
m := {
'a': '1'
'b': '2'
'c': '3'
}
assert db.hset('map', m)! == m.len
assert db.hget[string]('map', 'a')! == '1'
assert db.hget[string]('map', 'b')! == '2'
assert db.hget[string]('map', 'c')! == '3'
assert db.hgetall[string]('map')! == m
// test expire
assert db.expire('int', 1)!
time.sleep(2 * time.second)
db.get[i64]('int') or {
// tolerate server-specific message wording; ensure some message exists
assert err.msg().len > 0
}
// test del
assert db.del('string')! == 1
db.get[string]('string') or {
// tolerate server-specific message wording; ensure some message exists
assert err.msg().len > 0
}
// test custom cmd
assert db.cmd('SET', 'bigint', '123456')! as string == 'OK'
assert db.cmd('GET', 'bigint')! as []u8 == [u8(49), 50, 51, 52, 53, 54]
db.close()!
}
// A concise pipeline test that exercises correctness but remains fast
fn test_pipeline_small() {
mut db := redis.connect(password: redis_password)!
defer { db.close() or {} }
assert db.ping()! == 'PONG'
// start pipleline mode
db.pipeline_start()
// small pipeline of a few commands
db.cmd('FLUSHALL') or { eprintln('FLUSHALL failed (pipeline): ${err}') }
db.set(k_int, 1) or { eprintln('SET k_int failed (pipeline): ${err}') }
db.set(k_string, 'p') or { eprintln('SET k_string failed (pipeline): ${err}') }
db.get[i64](k_int) or { eprintln('GET k_int failed (pipeline): ${err}') }
db.get[string](k_string) or { eprintln('GET k_string failed (pipeline): ${err}') }
res := db.pipeline_execute()!
// Expect exactly the five results we queued and validate them precisely.
assert res.len == 5
match res[0] {
string { assert res[0] as string == 'OK' }
else { assert false }
}
match res[1] {
string { assert res[1] as string == 'OK' }
else { assert false }
}
match res[2] {
string { assert res[2] as string == 'OK' }
else { assert false }
}
match res[3] {
[]u8 { assert (res[3] as []u8).bytestr() == '1' }
else { assert false }
}
match res[4] {
[]u8 { assert (res[4] as []u8).bytestr() == 'p' }
else { assert false }
}
}
// Another small pipeline sequence test (fast)
fn test_pipeline_sequence() {
mut db := redis.connect(password: redis_password)!
defer { db.close() or {} }
// keep operations minimal and deterministic
db.cmd('FLUSHALL') or { eprintln('FLUSHALL failed: ${err}') }
assert db.set(k_counter, '0')! == 'OK'
// restart pipeline again
db.pipeline_start()
// test set[T]
db.set('int', 123)!
db.set('string', 'abc')!
db.set('bin', [u8(0x00), 0x01, 0x02, 0x03])!
// execute the queued pipeline commands and collect results
res := db.pipeline_execute()!
// We queued 3 SET commands above; each should return "OK"
assert res.len == 3
match res[0] {
string { assert res[0] as string == 'OK' }
else { assert false }
}
match res[1] {
string { assert res[1] as string == 'OK' }
else { assert false }
}
match res[2] {
string { assert res[2] as string == 'OK' }
else { assert false }
}
}
// -------------------- RESP3 / PUBSUB SHAPE CHECKS (FAST) --------------------
// Quick RESP3 ping and tiny roundtrip
fn test_resp3_ping() {
mut db := redis.connect(password: redis_password)!
defer { db.close() or {} }
// Basic ping and quick SET/GET roundtrip
p := db.ping() or { panic(err) }
assert p == 'PONG'
db.cmd('DEL', r_k_map) or { eprintln('DEL r_k_map failed: ${err}') }
assert db.cmd('SET', r_k_map, 'temp')! as string == 'OK'
val := db.cmd('GET', r_k_map)!
match val {
[]u8 { assert val.bytestr() == 'temp' }
string { assert val == 'temp' }
else { assert false }
}
db.cmd('DEL', r_k_map) or { eprintln('cleanup DEL r_k_map failed: ${err}') }
}
// Quick RESP3 HGETALL shape check (fast)
fn test_resp3_hgetall_fast() {
mut db := redis.connect(password: redis_password)!
defer { db.close() or {} }
db.cmd('DEL', r_k_map)!
assert db.hset(r_k_map, {
'k1': 'v1'
'k2': 'v2'
})! >= 1
if db.version != 3 {
res := db.hgetall[string](r_k_map) or { panic(err) }
assert res['k1'] == 'v1'
db.cmd('DEL', r_k_map)!
return
}
r := db.cmd('HGETALL', r_k_map) or { panic(err) }
match r {
map[string]redis.RedisValue {
v1 := r['k1']!
v2 := r['k2']!
assert (v1 as []u8).bytestr() == 'v1'
assert (v2 as []u8).bytestr() == 'v2'
}
redis.RedisMap {
assert (r.pairs[1] as []u8).bytestr() == 'v1'
assert (r.pairs[3] as []u8).bytestr() == 'v2'
}
[]redis.RedisValue {
assert (r[1] as []u8).bytestr() == 'v1'
assert (r[3] as []u8).bytestr() == 'v2'
}
else {
assert false
}
}
db.cmd('DEL', r_k_map)!
}
// Quick RESP3 SMEMBERS shape check (fast)
fn test_resp3_smembers_fast() {
mut db := redis.connect(password: redis_password)!
defer { db.close() or {} }
db.cmd('DEL', r_k_set) or { eprintln('DEL r_k_set failed: ${err}') }
db.cmd('SADD', r_k_set, 'one', 'two') or { eprintln('SADD r_k_set failed: ${err}') }
r := db.cmd('SMEMBERS', r_k_set) or { panic(err) }
match r {
redis.RedisSet {
mut f1 := false
mut f2 := false
for el in r.elements {
el_str := (el as []u8).bytestr()
if el_str == 'one' {
f1 = true
}
if el_str == 'two' {
f2 = true
}
}
assert f1 && f2
}
[]redis.RedisValue {
mut f1 := false
mut f2 := false
for el in r {
el_str := (el as []u8).bytestr()
if el_str == 'one' {
f1 = true
}
if el_str == 'two' {
f2 = true
}
}
assert f1 && f2
}
else {
assert false
}
}
db.cmd('DEL', r_k_set) or { eprintln('cleanup DEL r_k_set failed: ${err}') }
}
// Very small pub/sub smoke test (fast)
fn test_resp3_pubsub_fast() {
mut sub := redis.connect(password: redis_password)!
defer { sub.close() or {} }
if sub.version != 3 {
return
}
spawn publish_after_delay()
sub.cmd('SUBSCRIBE', r_k_pubch) or { eprintln('SUBSCRIBE failed: ${err}') }
time.sleep(200 * time.millisecond)
sub.cmd('PING') or { eprintln('PING (pubsub) failed: ${err}') }
}
// -------------------- SLOW / HEAVY TESTS --------------------
// Large binary bulk string roundtrip (~200 KiB)
fn test_large_binary_bulk_string() ! {
mut db := redis.connect(password: redis_password)!
defer { db.close() or {} }
// ensure clean DB
db.cmd('FLUSHALL')!
// Create a large binary payload (~200 KB) with a repeating pattern,
// including zero bytes and CRLF sequences to exercise edge cases.
mut data := []u8{len: 204800, init: u8(index & 0xFF)}
// inject some CRLF and zero-byte sequences at several spots
data[50] = `\r`
data[51] = `\n`
data[1000] = 0
data[1001] = `\r`
data[1002] = `\n`
// store binary data
assert db.set('bigbin', data.clone())! == 'OK'
// retrieve as []u8 using typed get
got := db.get[[]u8]('bigbin')!
assert got == data
// also test with small binary containing embedded CRLF and NUL
small := [u8(0), `a`, `b`, `\r`, `\n`, `c`]
assert db.set('smallbin', small)! == 'OK'
got_small := db.get[[]u8]('smallbin')!
assert got_small == small
// cleanup
db.del('bigbin')!
db.del('smallbin')!
}
// Very large payload (~1.2 MiB) roundtrip
fn test_very_large_payload() ! {
mut db := redis.connect(password: redis_password)!
defer { db.close() or {} }
// ensure clean DB
db.cmd('FLUSHALL')!
// Build a very large payload (> 1 MiB). Use ~1.2 MiB to be safely above 1 MiB.
size := 1_200_000
mut big := []u8{len: size, init: u8(index & 0xFF)}
// Put a few sentinel checks
big[0] = 0
big[1023] = `\r`
big[1024] = `\n`
big[size - 1] = 255
// store and retrieve
assert db.set('hugebin', big.clone())! == 'OK'
got := db.get[[]u8]('hugebin')!
assert got == big
// cleanup
db.del('hugebin')!
}
// Many pipeline commands to exercise batching behavior
fn test_many_pipeline_commands() ! {
mut db := redis.connect(password: redis_password)!
defer { db.close() or {} }
// ensure clean DB
db.cmd('FLUSHALL')!
// We'll queue a moderate but substantial number of commands in pipeline to test behavior.
// Keep it reasonable for CI runtime; 300 pairs (SET, GET) should exercise pipeline thoroughly.
count := 300
db.pipeline_start()
for i in 0 .. count {
key := 'kp_${i}'
val := 'v${i}'
db.cmd('SET', key, val)!
db.cmd('GET', key)!
}
results := db.pipeline_execute()!
// Expect 2 * count responses
assert results.len == count * 2
for i in 0 .. count {
set_idx := i * 2
get_idx := i * 2 + 1
// SET should return OK
match results[set_idx] {
string { assert (results[set_idx] as string) == 'OK' }
else { assert false }
}
// GET should return the value as []u8
expected := ('v${i}').bytes()
match results[get_idx] {
[]u8 { assert (results[get_idx] as []u8) == expected }
else { assert false }
}
}
}
// Larger randomized binary payloads (many iterations)
fn test_fuzz_random_binary_many() ! {
mut db := redis.connect(password: redis_password)!
defer { db.close() or {} }
db.cmd('FLUSHALL')!
// LCG PRNG seed using process id
mut x := u64(os.getpid())
count := 50
for iter in 0 .. count {
// size up to ~200 KiB
size := int((x % 200_000) + 1)
mut data := []u8{len: size}
for i in 0 .. size {
data[i] = rand.u8()
}
key := 'fuzz_big_${iter}'
assert db.set(key, data.clone())! == 'OK'
got := db.get[[]u8](key)!
assert got == data
// clean up
db.del(key)!
x += u64(size) * u64(iter + 1)
}
}
// Randomized tests that ensure CRLF sequences embedded at random locations,
// including chunk boundaries, are handled correctly.
fn test_fuzz_crlf_random_positions() ! {
mut db := redis.connect(password: redis_password)!
defer { db.close() or {} }
db.cmd('FLUSHALL')!
seed := u64(os.getpid())
mut x := seed
trials := 30
for t in 0 .. trials {
// size between 1 KiB and 32 KiB
size := int((x % 31_744) + 1024)
mut data := []u8{len: size}
for i in 0 .. size {
data[i] = rand.u8()
}
// place a CRLF at a pseudo-random position; sometimes force it near boundaries
pos := int(x % u64(size - 2))
data[pos] = `\r`
data[pos + 1] = `\n`
// also occasionally place CRLF at a chunk boundary (4096)
if t % 5 == 0 && size > 4097 {
data[4095] = `\r`
data[4096] = `\n`
}
key := 'fuzz_crlf_${t}'
assert db.set(key, data.clone())! == 'OK'
got := db.get[[]u8](key)!
assert got == data
db.del(key)!
x += u64(pos) + 1
}
}
// CRLF crossing a read-chunk boundary
fn test_crlf_at_chunk_boundary() {
mut db := redis.connect(password: redis_password)!
defer { db.close() or {} }
// ensure clean DB
db.cmd('FLUSHALL')!
// The driver's read buffer reads in 4096-byte chunks. Create a payload where CRLF
// crosses the 4096-byte boundary: place '\r' at index 4095 and '\n' at 4096.
chunk := 4096
size := chunk * 3 // a few chunks
mut data := []u8{len: size, init: u8((index * 37) % 256)}
if size > chunk + 1 {
data[chunk - 1] = `\r` // index 4095
data[chunk] = `\n` // index 4096
}
// store and retrieve
assert db.set('crlf_boundary', data.clone())! == 'OK'
got := db.get[[]u8]('crlf_boundary')!
assert got.len == data.len
// ensure boundary bytes preserved
assert got[chunk - 1] == `\r`
assert got[chunk] == `\n`
// cleanup
db.del('crlf_boundary')!
}
// Small randomized binary payloads (many repetitions).
fn test_fuzz_random_binary_small() {
mut db := redis.connect(password: redis_password)!
defer { db.close() or {} }
db.cmd('FLUSHALL')!
// Simple LCG PRNG seeded from process id
seed := u64(os.getpid())
mut x := seed
for iter := 0; iter < 20; iter++ {
// pseudo-random size in [1, 4096]
size := int((x % 4096) + 1)
mut data := []u8{len: size}
for i in 0 .. size {
data[i] = rand.u8()
}
k := 'fuzz_small_${iter}'
assert db.set(k, data.clone())! == 'OK'
got := db.get[[]u8](k)!
assert got == data
// mutate seed slightly so sizes vary
x += u64(size)
}
db.pipeline_start()
// ensure we have a small map to use here (define inline to avoid depending on outer scope)
db.hset('map', {
'a': '1'
'b': '2'
'c': '3'
})!
db.hget[string]('map', 'a')!
db.hget[string]('map', 'b')!
db.hget[string]('map', 'c')!
db.hgetall[string]('map')!
// test custom cmd
db.cmd('SET', 'bigint', '123456')!
db.cmd('GET', 'bigint')!
b := db.pipeline_execute()!
// Validate elements individually to accommodate server-shape differences.
assert b.len == 7
match b[0] {
i64 { assert (b[0] as i64) == 3 }
else { assert false }
}
match b[1] {
[]u8 { assert (b[1] as []u8) == [u8(49)] }
else { assert false }
}
match b[2] {
[]u8 { assert (b[2] as []u8) == [u8(50)] }
else { assert false }
}
match b[3] {
[]u8 { assert (b[3] as []u8) == [u8(51)] }
else { assert false }
}
// HGETALL may arrive as map[string]RedisValue, RedisMap, or array - accept common shapes.
match b[4] {
map[string]redis.RedisValue {
m := b[4] as map[string]redis.RedisValue
v := m['a'] or { panic('hgetall: missing a') }
assert (v as []u8) == [u8(49)]
}
redis.RedisMap {
rm := b[4] as redis.RedisMap
// pairs are interleaved key/value; expect at least 2 pairs and check the first value
assert rm.pairs.len >= 2
assert (rm.pairs[1] as []u8) == [u8(49)]
}
[]redis.RedisValue {
arr := b[4] as []redis.RedisValue
// RESP2-style interleaved array; ensure element 1 (second item) is the value for 'a'
assert arr.len > 1
assert (arr[1] as []u8) == [u8(49)]
}
else {
assert false
}
}
match b[5] {
string { assert b[5] as string == 'OK' }
else { assert false }
}
match b[6] {
[]u8 { assert (b[6] as []u8) == [u8(49), 50, 51, 52, 53, 54] }
else { assert false }
}
db.close()!
}
+84
View File
@@ -0,0 +1,84 @@
## Description
`sqlite` is a thin wrapper for [the SQLite library](https://sqlite.org/), which in turn is
"a C-language library that implements a small, fast, self-contained,
high-reliability, full-featured, SQL database engine."
# Install SQLite Dependency
On **any platform** (Windows, Linux, macOS), you can run:
`v vlib/db/sqlite/install_thirdparty_sqlite.vsh`
This downloads the SQLite amalgamation source and places it in
`v/thirdparty/sqlite`. V will then compile it automatically
during your build.
If you need to install SQLite manually, use the SQLite
**amalgamation** package. `v/thirdparty/sqlite` must contain
`sqlite3.c` and `sqlite3.h` directly.
Do not use the autoconf/source package here, and do not rename
`sqlite3.c` to `sqlite3.cpp`.
On **macOS**, V can also fall back to the system `libsqlite3`
when `pkg-config` is not available, so an extra install is often
not needed.
On **Linux**, you can also install the system development package
instead:
- Debian/Ubuntu: `sudo apt install -y libsqlite3-dev`
- Fedora/RHEL: `sudo dnf -y install sqlite-devel`
- Arch: `sudo pacman -S sqlite`
# Convenience Methods
The `DB` struct provides several helper methods for common
introspection queries:
```v
import db.sqlite
db := sqlite.connect('mydb.db') or { panic(err) }
// List all user tables
tables := db.tables()!
// Get column names for a table
cols := db.columns('users')!
// Get CREATE statements (single table or all objects)
s := db.schema('users')!
// Database file size in bytes
size := db.db_size()!
```
# Interactive CLI
V includes a built-in SQLite CLI as a replacement for `sqlite3`:
```sh
v sqlite mydb.db
```
Features include a full readline REPL with history and tab
completion, 9 output modes (`table`, `box`, `markdown`, `csv`,
`json`, `line`, `html`, `insert`, `quote`), `.dump`,
`.import`/`.export`, `.backup`, session control, and schema tools.
Run `.help` inside the REPL for the full command list.
# Performance Tips
When performing a large amount of database calls (i.e. INSERTS), significant
performance increase can be obtained by controlling the synchronization and journal modes.
For instance:
```v
import db.sqlite
db := sqlite.connect('foo.db') or { panic(err) }
db.synchronization_mode(sqlite.SyncMode.off)!
db.journal_mode(sqlite.JournalMode.memory)!
```
@@ -0,0 +1,60 @@
#!/usr/bin/env v
import os
import net.http
import compress.szip
import crypto.sha3
fn should_be_ok(http_status_code int, msg string) {
assert http_status_code == 200, '${msg}. Check your internet connection and try again later.'
}
fn main() {
os.chdir(@VEXEROOT)!
download_page_url := 'https://sqlite.org/download.html'
println('> Getting ${download_page_url} ...')
download_page := http.get(download_page_url)!
should_be_ok(download_page.status_code, 'The download page of SQLite is not available now.')
dlines := download_page.body.split_into_lines()
amalgamation_csv := dlines.filter(|line| line.starts_with('PRODUCT,')
&& line.contains('amalgamation'))[0].split(',')
assert amalgamation_csv.len >= 4
version := amalgamation_csv[1]
zip_name := os.file_name(amalgamation_csv[2])
zip_url := 'https://sqlite.org/${amalgamation_csv[2]}'
url_size := amalgamation_csv[3].int()
url_sha3 := amalgamation_csv[4]
println('> Getting SQLite amalgamation version: ${version}')
println('> from url: ${zip_url}')
println('> expected size: ${url_size}')
println('> expected SHA3: ${url_sha3} ...')
amalgamation_name := zip_name.to_lower().replace('.zip', '')
println('> Downloading from ${zip_url} ...')
zip_content := http.get(zip_url)!
should_be_ok(zip_content.status_code, 'The .zip file URL of SQLite is not available now.')
assert zip_content.body.len == url_size
println('> download size: ${zip_content.body.len} matches expected size: ${url_size} .')
zip_shasum := sha3.sum256(zip_content.body.bytes()).hex()
assert zip_shasum == url_sha3
println('> download sha3: ${zip_shasum} matches too.')
os.write_file(zip_name, zip_content.body)!
assert os.is_file(zip_name)
assert szip.extract_zip_to_dir(zip_name, 'thirdparty')!
os.rmdir_all('thirdparty/sqlite') or {}
os.mv('thirdparty/${amalgamation_name}', 'thirdparty/sqlite')!
os.rm('thirdparty/sqlite/shell.c')!
files := os.walk_ext('thirdparty/sqlite', '')
for f in files {
println('> extracted file: ${f:-40s} | size: ${os.file_size(f):8}')
}
println('> removing ${zip_name} ...')
os.rm(zip_name)!
println('> done')
}
+295
View File
@@ -0,0 +1,295 @@
module sqlite
import orm
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)
$if trace_orm_where ? {
eprintln('> sqlite.select: where.fields.len = ${where.fields.len}')
eprintln('> sqlite.select: where.kinds.len = ${where.kinds.len}')
}
// 1. Create query and bind necessary data
query := orm.orm_select_gen(config, '`', true, '?', 1, where_with_tenant)
$if trace_sqlite ? {
eprintln('> select query: "${query}"')
}
stmt := db.new_init_stmt(query)!
defer {
stmt.finalize()
}
mut c := 1
sqlite_stmt_binder(stmt, where_with_tenant, query, mut c)!
sqlite_stmt_binder(stmt, data, query, mut c)!
mut ret := [][]orm.Primitive{}
for {
// 2. Parse returned values
step := stmt.step()
if step == sqlite_done {
break
}
if step != sqlite_ok && step != sqlite_row {
break
}
mut row := []orm.Primitive{}
for i, typ in config.types {
primitive := stmt.sqlite_select_column(i, typ)!
row << primitive
}
ret << row
}
return ret
}
// sql stmt
// insert is used internally by V's ORM for processing `INSERT ` queries
pub fn (db DB) insert(table orm.Table, data orm.QueryData) ! {
query, converted_data :=
orm.orm_stmt_gen(.sqlite, table, '`', .insert, true, '?', 1, data, orm.QueryData{})
sqlite_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(.sqlite, table, '`', .update, true, '?', 1, data,
where_with_tenant)
sqlite_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(.sqlite, table, '`', .delete, true, '?', 1, orm.QueryData{},
where_with_tenant)
sqlite_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_rowid();'
return db.q_int(query) or { 0 }
}
// DDL (table creation/destroying etc)
// 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(.sqlite, table, '`', true, 0, fields, sqlite_type_from_v, false) or {
return err
}
sqlite_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}`;'
sqlite_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)!
}
// helper
// Executes query and bind prepared statement data directly
fn sqlite_stmt_worker(db DB, query string, data orm.QueryData, where orm.QueryData) ! {
$if trace_sqlite ? {
eprintln('> sqlite_stmt_worker query: "${query}"')
}
stmt := db.new_init_stmt(query)!
defer {
stmt.finalize()
}
mut c := 1
sqlite_stmt_binder(stmt, data, query, mut c)!
sqlite_stmt_binder(stmt, where, query, mut c)!
stmt.orm_step(query)!
}
// Binds all values of d in the prepared statement
fn sqlite_stmt_binder(stmt Stmt, d orm.QueryData, query string, mut c &int) ! {
for data in d.data {
err := bind(stmt, mut c, data)
if err != 0 {
return stmt.db.error_message(err, query)
}
if !sqlite_primitive_is_array(data) {
c++
}
}
}
fn sqlite_primitive_is_array(data orm.Primitive) bool {
return match data {
[]orm.Primitive, []bool, []f32, []f64, []i16, []i64, []i8, []int, []string, []time.Time,
[]u16, []u32, []u64, []u8, []orm.InfixType {
true
}
else {
false
}
}
}
fn bind_array[T](stmt Stmt, mut c &int, data []T) int {
mut err := 0
for element in data {
tmp_err := bind(stmt, mut c, orm.Primitive(element))
c++
if tmp_err != 0 {
err = tmp_err
break
}
}
return err
}
// Universal bind function
fn bind(stmt Stmt, mut c &int, data orm.Primitive) int {
mut err := 0
match data {
i8, i16, int, u8, u16, u32, bool {
err = stmt.bind_int(c, int(data))
}
i64, u64 {
err = stmt.bind_i64(c, i64(data))
}
f32, f64 {
err = stmt.bind_f64(c, f64(data))
}
string {
err = stmt.bind_text(c, data)
}
time.Time {
err = stmt.bind_int(c, int(data.unix()))
}
orm.InfixType {
err = bind(stmt, mut c, data.right)
}
orm.Null {
err = stmt.bind_null(c)
}
[]orm.Primitive {
err = bind_array(stmt, mut c, data)
}
[]bool {
err = bind_array(stmt, mut c, data)
}
[]f32 {
err = bind_array(stmt, mut c, data)
}
[]f64 {
err = bind_array(stmt, mut c, data)
}
[]i16 {
err = bind_array(stmt, mut c, data)
}
[]i64 {
err = bind_array(stmt, mut c, data)
}
[]i8 {
err = bind_array(stmt, mut c, data)
}
[]int {
err = bind_array(stmt, mut c, data)
}
[]string {
err = bind_array(stmt, mut c, data)
}
[]time.Time {
err = bind_array(stmt, mut c, data)
}
[]u16 {
err = bind_array(stmt, mut c, data)
}
[]u32 {
err = bind_array(stmt, mut c, data)
}
[]u64 {
err = bind_array(stmt, mut c, data)
}
[]u8 {
err = bind_array(stmt, mut c, data)
}
[]orm.InfixType {
err = bind_array(stmt, mut c, data)
}
}
return err
}
// Selects column in result and converts it to an orm.Primitive
fn (stmt Stmt) sqlite_select_column(idx int, typ int) !orm.Primitive {
if typ in orm.nums || typ == -1 {
return stmt.get_int(idx) or { return orm.Null{} }
} else if typ in orm.num64 {
return stmt.get_i64(idx) or { return orm.Null{} }
} else if typ == typeof[f32]().idx {
return f32(stmt.get_f64(idx) or { return orm.Null{} })
} else if typ == typeof[f64]().idx {
return stmt.get_f64(idx) or { return orm.Null{} }
} else if typ == orm.type_string {
if v := stmt.get_text(idx) {
return v.clone()
} else {
return orm.Null{}
}
} else if typ == orm.enum_ {
return stmt.get_i64(idx) or { return orm.Null{} }
} else if typ == orm.time_ {
if v := stmt.get_int(idx) {
return time.unix(v)
} else {
return orm.Null{}
}
} else {
return error('Unknown type ${typ}')
}
}
// Convert type int to sql type string
fn sqlite_type_from_v(typ int) !string {
return if typ in orm.nums || typ in orm.num64 || typ in [orm.serial, orm.time_, orm.enum_] {
'INTEGER'
} else if typ in orm.float {
'REAL'
} else if typ == orm.type_string {
'TEXT'
} else {
error('Unknown type ${typ}')
}
}
@@ -0,0 +1,93 @@
// vtest build: present_sqlite3?
import db.sqlite
struct Parent {
id int @[primary; sql: serial]
name string
children []Child @[fkey: 'parent_id']
}
struct Child {
id int @[primary; sql: serial]
name string
parent_id int
babies []Baby @[fkey: 'child_id']
}
struct Baby {
id int @[primary; sql: serial]
name string
child_id int
}
fn test_main() {
mut db := sqlite.connect(':memory:')!
sql db {
create table Parent
create table Child
create table Baby
} or { panic(err) }
new_parent := Parent{
name: 'first parent'
children: [
Child{
name: 'first child'
},
Child{
name: 'second_child'
},
]
}
sql db {
insert new_parent into Parent
} or { panic(err) }
babies := [
Baby{
name: 'first baby'
child_id: 1
},
Baby{
name: 'second baby'
child_id: 1
},
Baby{
name: 'third baby'
child_id: 2
},
Baby{
name: 'fourth baby'
child_id: 2
},
Baby{
name: 'fifth baby'
child_id: 2
},
]
for v in babies {
sql db {
insert v into Baby
} or { panic(err) }
}
parent := sql db {
select from Parent
} or { panic(err) }
assert parent[0].children[0].id == 1
assert parent[0].children[1].id == 2
assert parent[0].children.len == 2
assert parent[0].children[0].babies.len == 2
assert parent[0].children[1].babies.len == 3
assert parent[0].children[0].babies[0].name == 'first baby'
assert parent[0].children[0].babies[1].name == 'second baby'
assert parent[0].children[1].babies[0].name == 'third baby'
assert parent[0].children[1].babies[1].name == 'fourth baby'
assert parent[0].children[1].babies[2].name == 'fifth baby'
db.close()!
}
+38
View File
@@ -0,0 +1,38 @@
module sqlite
pub struct ConnectionPool {
mut:
connections chan DB
config string
}
// new_connection_pool creates a new connection pool with the given size and configuration.
pub fn new_connection_pool(config string, 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 { panic('Failed to close connection: ${err}') }
}
}
@@ -0,0 +1,30 @@
// vtest build: present_sqlite3? && gcc && !windows
// vtest vflags: -prod
import db.sqlite
@[table: 'issue_24800_foos']
struct Issue24800Foo {
id int @[primary; sql: serial]
name string
}
struct Issue24800App {
mut:
db sqlite.DB
}
fn issue_24800_memory_app() !Issue24800App {
db := sqlite.connect(':memory:')!
sql db {
create table Issue24800Foo
}!
return Issue24800App{
db: db
}
}
fn test_issue_24800_prod_gcc_orm_create_table() {
mut app := issue_24800_memory_app()!
assert app.db.q_int("select count(*) from sqlite_master where type='table' and name='issue_24800_foos'")! == 1
app.db.close()!
}
+118
View File
@@ -0,0 +1,118 @@
module sqlite
// Result represents Sqlite Result and Error Codes
// see https://www.sqlite.org/rescode.html
pub enum Result {
ok = 0
error = 1
internal = 2
perm = 3
abort = 4
busy = 5
locked = 6
nomem = 7
readonly = 8
interrupt = 9
ioerr = 10
corrupt = 11
notfound = 12
full = 13
cantopen = 14
protocol = 15
empty = 16
schema = 17
toobig = 18
constraint = 19
mismatch = 20
misuse = 21
nolfs = 22
auth = 23
format = 24
range = 25
notadb = 26
notice = 27
warning = 28
row = 100
done = 101
ok_load_permanently = 256
error_missing_collseq = 257
busy_recovery = 261
locked_sharedcache = 262
readonly_recovery = 264
ioerr_read = 266
corrupt_vtab = 267
cantopen_notempdir = 270
constraint_check = 275
notice_recover_wal = 283
warning_autoindex = 284
error_retry = 513
abort_rollback = 516
busy_snapshot = 517
locked_vtab = 518
readonly_cantlock = 520
ioerr_short_read = 522
corrupt_sequence = 523
cantopen_isdir = 526
constraint_commithook = 531
notice_recover_rollback = 539
error_snapshot = 769
busy_timeout = 773
readonly_rollback = 776
ioerr_write = 778
corrupt_index = 779
cantopen_fullpath = 782
constraint_foreignkey = 787
readonly_dbmoved = 1032
ioerr_fsync = 1034
cantopen_convpath = 1038
constraint_function = 1043
readonly_cantinit = 1288
ioerr_dir_fsync = 1290
cantopen_dirtywal = 1294
constraint_notnull = 1299
readonly_directory = 1544
ioerr_truncate = 1546
cantopen_symlink = 1550
constraint_primarykey = 1555
ioerr_fstat = 1802
constraint_trigger = 1811
ioerr_unlock = 2058
constraint_unique = 2067
ioerr_rdlock = 2314
constraint_vtab = 2323
ioerr_delete = 2570
constraint_rowid = 2579
ioerr_blocked = 2826
constraint_pinned = 2835
ioerr_nomem = 3082
ioerr_access = 3338
ioerr_checkreservedlock = 3594
ioerr_lock = 3850
ioerr_close = 4106
ioerr_dir_close = 4362
ioerr_shmopen = 4618
ioerr_shmsize = 4874
ioerr_shmlock = 5130
ioerr_shmmap = 5386
ioerr_seek = 5642
ioerr_delete_noent = 5898
ioerr_mmap = 6154
ioerr_gettemppath = 6410
ioerr_convpath = 6666
ioerr_vnode = 6922
ioerr_auth = 7178
ioerr_begin_atomic = 7434
ioerr_commit_atomic = 7690
ioerr_rollback_atomic = 7946
ioerr_data = 8202
}
// is_error checks if it is an error code.
pub fn (r Result) is_error() bool {
return r !in [.ok, .row, .done]
}
// is_error checks if `code` is an error code.
pub fn is_error(code int) bool {
return unsafe { Result(code).is_error() }
}
+672
View File
@@ -0,0 +1,672 @@
module sqlite
$if freebsd || openbsd {
#flag -I/usr/local/include
#flag -L/usr/local/lib
}
$if tinyc {
#flag -DSQLITE_DISABLE_INTRINSIC
}
$if $pkgconfig('sqlite3') {
#pkgconfig sqlite3
#include "sqlite3.h" # The SQLite header file is missing. Please install the corresponding development package.
} $else $if windows {
#flag -I@VEXEROOT/thirdparty/sqlite
#flag @VEXEROOT/thirdparty/sqlite/sqlite3.o
#include "sqlite3.h" # The SQLite header file is missing. Please run vlib/db/sqlite/install_thirdparty_sqlite.vsh to download an SQLite amalgamation.
} $else $if darwin {
// macOS ships libsqlite3, so do not require a separately downloaded amalgamation.
#flag darwin -lsqlite3
} $else {
#flag -I@VEXEROOT/thirdparty/sqlite
#include "sqlite3.h" # The SQLite header file is missing. Please run vlib/db/sqlite/install_thirdparty_sqlite.vsh to download an SQLite amalgamation.
#flag @VEXEROOT/thirdparty/sqlite/sqlite3.c
}
$if linux {
#flag -lm
}
// https://www.sqlite.org/rescode.html
pub const sqlite_ok = 0
pub const sqlite_error = 1
pub const sqlite_row = 100
pub const sqlite_done = 101
pub const sqlite_cantopen = 14
pub const sqlite_ioerr_read = 266
pub const sqlite_ioerr_short_read = 522
pub const sqlite_ioerr_write = 778
pub const sqlite_ioerr_fsync = 1034
pub const sqlite_ioerr_fstat = 1802
pub const sqlite_ioerr_delete = 2570
pub const sqlite_open_main_db = 0x00000100
pub const sqlite_open_temp_db = 0x00000200
pub const sqlite_open_transient_db = 0x00000400
pub const sqlite_open_main_journal = 0x00000800
pub const sqlite_open_temp_journal = 0x00001000
pub const sqlite_open_subjournal = 0x00002000
pub const sqlite_open_super_journal = 0x00004000
pub const sqlite_open_wal = 0x00080000
pub enum SyncMode {
off
normal
full
}
pub enum Sqlite3TransactionLevel {
deferred
immediate
exclusive
}
pub enum JournalMode {
off
delete
truncate
persist
memory
}
pub struct C.sqlite3 {
}
pub struct C.sqlite3_stmt {
}
@[heap]
pub struct Stmt {
stmt &C.sqlite3_stmt = unsafe { nil }
db &DB = unsafe { nil }
}
struct SQLError {
MessageError
}
//
@[heap]
pub struct DB {
pub mut:
is_open bool
mut:
conn &C.sqlite3 = unsafe { nil }
}
// str returns a text representation of the DB
pub fn (db &DB) str() string {
return 'sqlite.DB{ conn: ' + ptr_str(db.conn) + ' }'
}
pub struct Row {
pub mut:
vals []string
names []string
}
// val returns the value at `index`.
pub fn (row Row) val(index int) string {
return row.vals[index]
}
// values returns all row values.
pub fn (row Row) values() []string {
return row.vals.clone()
}
// get_string returns the value for the given column name, or '' if the column is not found
// or if the corresponding value index is out of range.
pub fn (r &Row) get_string(col_name string) string {
for i, name in r.names {
if name == col_name {
if i < r.vals.len {
return r.vals[i]
}
return ''
}
}
return ''
}
// get_int returns the integer value for the given column name, or 0 if the column is not found.
pub fn (r &Row) get_int(col_name string) int {
return r.get_string(col_name).int()
}
pub type Params = []string | [][]string
fn sqlite_cstring(s string) &char {
cstr := unsafe { malloc_noscan(s.len + 1) }
unsafe {
if s.len > 0 {
vmemcpy(cstr, s.str, s.len)
}
cstr[s.len] = 0
}
return &char(cstr)
}
//
fn C.sqlite3_open(&char, &&C.sqlite3) i32
fn C.sqlite3_close(&C.sqlite3) i32
fn C.sqlite3_busy_timeout(db &C.sqlite3, ms i32) i32
fn C.sqlite3_last_insert_rowid(&C.sqlite3) i64
//
fn C.sqlite3_prepare_v2(&C.sqlite3, &char, i32, &&C.sqlite3_stmt, &&char) i32
fn C.sqlite3_step(&C.sqlite3_stmt) i32
fn C.sqlite3_reset(&C.sqlite3_stmt) i32
fn C.sqlite3_finalize(&C.sqlite3_stmt) i32
//
fn C.sqlite3_column_name(&C.sqlite3_stmt, i32) &char
fn C.sqlite3_column_text(&C.sqlite3_stmt, i32) &u8
fn C.sqlite3_column_int(&C.sqlite3_stmt, i32) i32
fn C.sqlite3_column_int64(&C.sqlite3_stmt, i32) i64
fn C.sqlite3_column_double(&C.sqlite3_stmt, i32) f64
fn C.sqlite3_column_count(&C.sqlite3_stmt) i32
fn C.sqlite3_column_type(&C.sqlite3_stmt, i32) i32
fn C.sqlite3_column_bytes(&C.sqlite3_stmt, i32) i32
//
fn C.sqlite3_errstr(i32) &char
fn C.sqlite3_errmsg(&C.sqlite3) &char
fn C.sqlite3_free(voidptr)
fn C.sqlite3_changes(&C.sqlite3) i32
// connect Opens the connection with a database.
pub fn connect(path string) !DB {
db := &C.sqlite3(unsafe { nil })
path_cstr := sqlite_cstring(path)
defer {
unsafe { free(path_cstr) }
}
code := C.sqlite3_open(path_cstr, &db)
if code != 0 {
return &SQLError{
msg: unsafe { cstring_to_vstring(&char(C.sqlite3_errmsg(db))) }
code: code
}
}
return DB{
conn: db
is_open: true
}
}
// close Closes the DB.
// TODO: For all functions, determine whether the connection is
// closed first, and determine what to do if it is
pub fn (mut db DB) close() ! {
code := C.sqlite3_close(db.conn)
if code == 0 {
db.is_open = false
} else {
return &SQLError{
msg: unsafe { cstring_to_vstring(&char(C.sqlite3_errmsg(db.conn))) }
code: code
}
}
}
// Only for V ORM
fn get_int_from_stmt(stmt &C.sqlite3_stmt) int {
x := C.sqlite3_step(stmt)
if x != C.SQLITE_OK && x != C.SQLITE_DONE {
C.puts(C.sqlite3_errstr(x))
}
res := C.sqlite3_column_int(stmt, 0)
C.sqlite3_finalize(stmt)
return res
}
// last_insert_rowid returns last inserted rowid
// https://www.sqlite.org/c3ref/last_insert_rowid.html
pub fn (db &DB) last_insert_rowid() i64 {
return C.sqlite3_last_insert_rowid(db.conn)
}
// get_affected_rows_count returns `sqlite changes()` meaning amount of rows affected by most recent sql query
pub fn (db &DB) get_affected_rows_count() int {
return C.sqlite3_changes(db.conn)
}
// q_int returns a single integer value, from the first column of the result of executing `query`, or an error on failure
pub fn (db &DB) q_int(query string) !int {
$if trace_sqlite ? {
eprintln('> q_int query: "${query}"')
}
stmt := &C.sqlite3_stmt(unsafe { nil })
pres := C.sqlite3_prepare_v2(db.conn, &char(query.str), query.len, &stmt, 0)
if pres != sqlite_ok {
return db.error_message(pres, query)
}
defer {
C.sqlite3_finalize(stmt)
}
code := C.sqlite3_step(stmt)
if code != sqlite_row {
return db.error_message(code, query)
}
res := C.sqlite3_column_int(stmt, 0)
return res
}
// q_string returns a single string value, from the first column of the result of executing `query`, or an error on failure
pub fn (db &DB) q_string(query string) !string {
$if trace_sqlite ? {
eprintln('> q_string query: "${query}"')
}
stmt := &C.sqlite3_stmt(unsafe { nil })
pres := C.sqlite3_prepare_v2(db.conn, &char(query.str), query.len, &stmt, 0)
if pres != sqlite_ok {
return db.error_message(pres, query)
}
defer {
C.sqlite3_finalize(stmt)
}
code := C.sqlite3_step(stmt)
if code != sqlite_row {
return db.error_message(code, query)
}
val := unsafe { &u8(C.sqlite3_column_text(stmt, 0)) }
return if val != &u8(unsafe { nil }) { unsafe { tos_clone(val) } } else { '' }
}
// exec_map executes the query on the given `db`, and returns an array of maps of strings, or an error on failure
pub fn (db &DB) exec_map(query string) ![]map[string]string {
$if trace_sqlite ? {
eprintln('> exec_map query: "${query}"')
}
stmt := &C.sqlite3_stmt(unsafe { nil })
mut code := C.sqlite3_prepare_v2(db.conn, &char(query.str), query.len, &stmt, 0)
if code != sqlite_ok {
return db.error_message(code, query)
}
defer {
C.sqlite3_finalize(stmt)
}
nr_cols := C.sqlite3_column_count(stmt)
mut col_names := []string{cap: nr_cols}
for i in 0 .. nr_cols {
col_char := unsafe { &u8(C.sqlite3_column_name(stmt, i)) }
col_names << if col_char != unsafe { nil } { unsafe { tos_clone(col_char) } } else { '' }
}
mut res := 0
mut rows := []map[string]string{}
for {
res = C.sqlite3_step(stmt)
if res != sqlite_row {
break
}
mut row := map[string]string{}
for i in 0 .. nr_cols {
val := unsafe { &u8(C.sqlite3_column_text(stmt, i)) }
row[col_names[i]] = if val != unsafe { nil } { unsafe { tos_clone(val) } } else { '' }
}
rows << row
}
return rows
}
fn C.sqlite3_memory_used() i64
// exec executes the query on the given `db`, and returns an array of all the results, or an error on failure
pub fn (db &DB) exec(query string) ![]Row {
$if trace_sqlite ? {
eprintln('> exec query: "${query}"')
}
stmt := &C.sqlite3_stmt(unsafe { nil })
mut code := C.sqlite3_prepare_v2(db.conn, &char(query.str), query.len, &stmt, 0)
if code != sqlite_ok {
return db.error_message(code, query)
}
defer {
C.sqlite3_finalize(stmt)
}
nr_cols := C.sqlite3_column_count(stmt)
mut col_names := []string{cap: nr_cols}
for i in 0 .. nr_cols {
col_char := unsafe { &u8(C.sqlite3_column_name(stmt, i)) }
col_names << if col_char != unsafe { nil } { unsafe { tos_clone(col_char) } } else { '' }
}
mut res := 0
mut rows := []Row{}
for {
res = C.sqlite3_step(stmt)
if res != sqlite_row {
break
}
mut row := Row{
names: col_names
}
for i in 0 .. nr_cols {
val := unsafe { &u8(C.sqlite3_column_text(stmt, i)) }
if val == &u8(unsafe { nil }) {
row.vals << ''
} else {
row.vals << unsafe { tos_clone(val) }
}
}
rows << row
}
return rows
}
// exec_one executes a query on the given `db`.
// It returns either the first row from the result, if the query was successful, or an error.
@[manualfree]
pub fn (db &DB) exec_one(query string) !Row {
rows := db.exec(query)!
defer {
unsafe { rows.free() }
}
if rows.len == 0 {
return &SQLError{
msg: 'No rows'
code: sqlite_done
}
}
res := rows[0]
return res
}
// error_message returns a proper V error, given an integer error code received from SQLite, and a query string
@[manualfree]
pub fn (db &DB) error_message(code int, query string) IError {
errmsg := unsafe { cstring_to_vstring(&char(C.sqlite3_errmsg(db.conn))) }
msg := '${errmsg} (${code}) (${query})'
unsafe { errmsg.free() }
return SQLError{
msg: msg
code: code
}
}
// exec_none executes a query, and returns the integer SQLite result code.
// Use it, in case you don't expect any row results, but still want a result code.
// e.g. for queries like these: `INSERT INTO ... VALUES (...)`
pub fn (db &DB) exec_none(query string) int {
$if trace_sqlite ? {
eprintln('> exec_none query: "${query}"')
}
stmt := &C.sqlite3_stmt(unsafe { nil })
pres := C.sqlite3_prepare_v2(db.conn, &char(query.str), query.len, &stmt, 0)
if pres != sqlite_ok {
return -1
}
defer {
C.sqlite3_finalize(stmt)
}
code := C.sqlite3_step(stmt)
return code
}
// exec_param_many executes a query with parameters provided as ?,
// and returns either an error on failure, or the full result set on success
pub fn (db &DB) exec_param_many(query string, params Params) ![]Row {
$if trace_sqlite ? {
eprintln('> exec_param_many query: "${query}", params: ${params}')
}
mut stmt := &C.sqlite3_stmt(unsafe { nil })
mut code := C.sqlite3_prepare_v2(db.conn, &char(query.str), -1, &stmt, 0)
if code != sqlite_ok {
return db.error_message(code, query)
}
defer {
C.sqlite3_finalize(stmt)
}
mut rows := []Row{}
nr_cols := C.sqlite3_column_count(stmt)
mut col_names := []string{cap: nr_cols}
for i in 0 .. nr_cols {
col_char := unsafe { &u8(C.sqlite3_column_name(stmt, i)) }
col_names << if col_char != unsafe { nil } { unsafe { tos_clone(col_char) } } else { '' }
}
if params is []string {
for i, param in params {
code = C.sqlite3_bind_text(stmt, i + 1, voidptr(param.str), param.len, 0)
if code != sqlite_ok {
return db.error_message(code, query)
}
}
for {
mut row := Row{
names: col_names
}
code = C.sqlite3_step(stmt)
if is_error(code) {
return db.error_message(code, query)
}
if code == sqlite_done {
break
}
for i in 0 .. nr_cols {
val := unsafe { &u8(C.sqlite3_column_text(stmt, i)) }
if val == &u8(unsafe { nil }) {
row.vals << ''
} else {
row.vals << unsafe { tos_clone(val) }
}
}
rows << row
}
} else if params is [][]string {
// Rows to process
for params_row in params {
mut row := Row{
names: col_names
}
// Param values to bind
for i, param in params_row {
code = C.sqlite3_bind_text(stmt, i + 1, voidptr(param.str), param.len, 0)
if code != sqlite_ok {
return db.error_message(code, query)
}
}
for {
code = C.sqlite3_step(stmt)
if is_error(code) {
return db.error_message(code, query)
}
if code == sqlite_done {
break
}
for i in 0 .. nr_cols {
val := unsafe { &u8(C.sqlite3_column_text(stmt, i)) }
if val == &u8(unsafe { nil }) {
row.vals << ''
} else {
row.vals << unsafe { tos_clone(val) }
}
}
rows << row
}
C.sqlite3_reset(stmt)
}
}
return rows
}
// exec_param executes a query with one parameter provided as a ?,
// and returns either an error on failure, or the full result set on success
pub fn (db &DB) exec_param(query string, param string) ![]Row {
return db.exec_param_many(query, [param])
}
// exec_param2 executes a query with two parameters provided as ? placeholders.
pub fn (db &DB) exec_param2(query string, param string, param2 string) ![]Row {
return db.exec_param_many(query, [param, param2])
}
// create_table issues a "create table if not exists" command to the db.
// It creates the table named 'table_name', with columns generated from 'columns' array.
// The default columns type will be TEXT.
pub fn (mut db DB) create_table(table_name string, columns []string) ! {
db.exec('create table if not exists ${table_name} (' + columns.join(',\n') + ')')!
}
// busy_timeout sets a busy timeout in milliseconds.
// Sleeps for a specified amount of time when a table is locked. The handler
// will sleep multiple times until at least "ms" milliseconds of sleeping have accumulated.
// (see https://www.sqlite.org/c3ref/busy_timeout.html)
pub fn (db &DB) busy_timeout(ms int) int {
return C.sqlite3_busy_timeout(db.conn, ms)
}
// synchronization_mode sets disk synchronization mode, which controls how
// aggressively SQLite will write data to physical storage.
// If the command fails to execute an error is returned
// .off: No syncs at all. (fastest)
// .normal: Sync after each sequence of critical disk operations.
// .full: Sync after each critical disk operation (slowest).
pub fn (db &DB) synchronization_mode(sync_mode SyncMode) ! {
if sync_mode == .off {
db.exec('pragma synchronous = OFF;')!
} else if sync_mode == .full {
db.exec('pragma synchronous = FULL;')!
} else {
db.exec('pragma synchronous = NORMAL;')!
}
}
// journal_mode controls how the journal file is stored and processed.
// If the command fails to execute an error is returned
// .off: No journal record is kept. (fastest)
// .memory: Journal record is held in memory, rather than on disk.
// .delete: At the conclusion of a transaction, journal file is deleted.
// .truncate: Journal file is truncated to a length of zero bytes.
// .persist: Journal file is left in place, but the header is overwritten to indicate journal is no longer valid.
pub fn (db &DB) journal_mode(journal_mode JournalMode) ! {
if journal_mode == .off {
db.exec('pragma journal_mode = OFF;')!
} else if journal_mode == .delete {
db.exec('pragma journal_mode = DELETE;')!
} else if journal_mode == .truncate {
db.exec('pragma journal_mode = TRUNCATE;')!
} else if journal_mode == .persist {
db.exec('pragma journal_mode = PERSIST;')!
} else if journal_mode == .memory {
db.exec('pragma journal_mode = MEMORY;')!
} else {
db.exec('pragma journal_mode = MEMORY;')!
}
}
@[params]
pub struct Sqlite3TransactionParam {
transaction_level Sqlite3TransactionLevel = .deferred
}
// begin begins a new transaction.
pub fn (mut db DB) begin(param Sqlite3TransactionParam) ! {
mut sql_stmt := 'BEGIN '
match param.transaction_level {
.deferred { sql_stmt += 'DEFERRED;' }
.immediate { sql_stmt += 'IMMEDIATE;' }
.exclusive { sql_stmt += 'EXCLUSIVE;' }
}
db.exec(sql_stmt)!
}
// savepoint create a new savepoint.
pub fn (mut db DB) savepoint(savepoint string) ! {
if !savepoint.is_identifier() {
return error('savepoint should be a identifier string')
}
db.exec('SAVEPOINT ${savepoint};')!
}
// commit commits the current transaction.
pub fn (mut db DB) commit() ! {
db.exec('COMMIT;')!
}
// rollback rollbacks the current transaction.
pub fn (mut db DB) rollback() ! {
db.exec('ROLLBACK;')!
}
// rollback_to rollbacks to a specified savepoint.
pub fn (mut db DB) rollback_to(savepoint string) ! {
if !savepoint.is_identifier() {
return error('savepoint should be a identifier string')
}
db.exec('ROLLBACK TO ${savepoint};')!
}
// release_savepoint releases a specified savepoint.
pub fn (mut db DB) release_savepoint(savepoint string) ! {
if !savepoint.is_identifier() {
return error('savepoint should be a identifier string')
}
db.exec('RELEASE SAVEPOINT ${savepoint};')!
}
// tables returns the names of all user tables in the database.
pub fn (db &DB) tables() ![]string {
rows :=
db.exec("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name")!
return rows.map(it.vals[0])
}
// columns returns the column names for the given table.
pub fn (db &DB) columns(table string) ![]string {
escaped := table.replace('"', '""')
rows := db.exec('PRAGMA table_info("${escaped}")')!
return rows.map(it.vals[1])
}
// schema returns the CREATE statement(s) for the given table, or for all
// objects if table is empty.
pub fn (db &DB) schema(table string) !string {
filter := if table != '' {
escaped := table.replace("'", "''")
"AND name='${escaped}'"
} else {
''
}
rows :=
db.exec("SELECT sql FROM sqlite_master WHERE type IN ('table','index','view','trigger') ${filter} AND sql IS NOT NULL ORDER BY type, name")!
return rows.map(it.vals[0]).join('\n\n')
}
// db_size returns the database file size in bytes, computed from page_count
// and page_size.
pub fn (db &DB) db_size() !i64 {
pc := db.exec('PRAGMA page_count')!
ps := db.exec('PRAGMA page_size')!
if pc.len == 0 || ps.len == 0 {
return 0
}
return pc[0].vals[0].i64() * ps[0].vals[0].i64()
}
// reset returns the connection to initial state for reuse
pub fn (mut db DB) reset() ! {
}
// validate checks if the connection is still usable
pub fn (mut db DB) validate() !bool {
return db.exec_none('SELECT 1') == 100
}
@@ -0,0 +1,45 @@
// vtest build: present_sqlite3?
module main
import db.sqlite
@[table: 'blog']
pub struct Blog {
id int @[primary; sql: serial]
slug string
language string
}
fn records_by_field[T](db sqlite.DB, fieldname string, value string) ![]T {
$for field in T.fields {
if field.name == fieldname {
entries := sql db {
select from Blog where field.name == value
} or { return err }
return entries
}
}
return error('fieldname not found')
}
fn test_main() {
mut db := sqlite.connect(':memory:')!
sql db {
create table Blog
}!
row := Blog{
slug: 'Test'
language: 'v'
}
sql db {
insert row into Blog
}!
rows := records_by_field[Blog](db, 'language', 'v') or {
println(err)
return
}
assert rows.len == 1
db.close()!
}
+25
View File
@@ -0,0 +1,25 @@
// vtest build: present_sqlite3? && !sanitize-memory-clang
import db.sqlite
struct Test {
value f32
value2 f64
}
fn test_main() {
conn := sqlite.connect(':memory:')!
data := Test{32.32, 64.64}
sql conn {
create table Test
insert data into Test
}!
s := sql conn {
select from Test
}!
assert s[0].value == 32.32
assert s[0].value2 == 64.64
}
@@ -0,0 +1,26 @@
// vtest build: present_sqlite3? && !windows && !sanitize-memory-clang && !docker-ubuntu-musl
import db.sqlite
@[table: 'foos']
struct Foo {
value int
}
struct State {
val ?int
}
fn test_main() {
val := ?int(none)
state := State{val}
db := sqlite.connect(':memory:')!
if state.val != none {
v := sql db {
select from Foo where value < state.val
}!
println(v)
assert false
} else {
assert true
}
}
+317
View File
@@ -0,0 +1,317 @@
// vtest build: present_sqlite3?
import orm
import db.sqlite
import time
struct TestCustomSqlType {
id int @[primary; sql: serial]
custom string @[sql_type: 'INTEGER']
custom1 string @[sql_type: 'TEXT']
custom2 string @[sql_type: 'REAL']
custom3 string @[sql_type: 'NUMERIC']
custom4 string
custom5 int
custom6 time.Time
}
struct TestDefaultAttribute {
id string @[primary; sql: serial]
name string
created_at ?string @[default: 'CURRENT_TIME']
created_at1 ?string @[default: 'CURRENT_DATE']
created_at2 ?string @[default: 'CURRENT_TIMESTAMP']
}
struct TestDurationAlias {
id int @[primary; sql: serial]
duration time.Duration
}
struct EntityToTest {
id int @[notnull; sql_type: 'INTEGER']
smth string @[notnull; sql_type: 'TEXT']
}
struct TutorialBlogArticle {
id int @[primary; sql: serial]
title string
text string
}
struct TutorialBlogApp {
mut:
db sqlite.DB
}
fn (app &TutorialBlogApp) find_all_tutorial_blog_articles() []TutorialBlogArticle {
return sql app.db {
select from TutorialBlogArticle
} or { panic(err) }
}
fn test_sqlite_orm() {
mut db := sqlite.connect(':memory:') or { panic(err) }
defer {
db.close() or { panic(err) }
}
table := orm.Table{
name: 'Test'
}
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[i64]().idx
},
]) or { panic(err) }
db.insert(table, orm.QueryData{
fields: ['name', 'age']
data: [orm.string_to_primitive('Louis'), orm.i64_to_primitive(100)]
}) 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(100)]
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 == 100
}
/** 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.exec('
pragma table_info(TestCustomSqlType);
')!
mut table_info_types_results := []string{}
information_schema_custom_sql := ['INTEGER', 'INTEGER', 'TEXT', 'REAL', 'NUMERIC', 'TEXT',
'INTEGER', 'INTEGER']
for data_type in result_custom_sql {
table_info_types_results << data_type.vals[2]
}
assert table_info_types_results == information_schema_custom_sql
sql db {
drop table TestCustomSqlType
}!
/** test default attribute
*/
sql db {
create table TestDefaultAttribute
}!
mut result_default_sql := db.exec('
pragma table_info(TestDefaultAttribute);
')!
mut information_schema_data_types_results := []string{}
information_schema_default_sql := ['', '', 'CURRENT_TIME', 'CURRENT_DATE', 'CURRENT_TIMESTAMP']
for data_type in result_default_sql {
information_schema_data_types_results << data_type.vals[4]
}
assert information_schema_data_types_results == information_schema_default_sql
test_default_attribute := TestDefaultAttribute{
name: 'Hitalo'
}
sql db {
insert test_default_attribute into TestDefaultAttribute
}!
test_default_attributes := sql db {
select from TestDefaultAttribute limit 1
}!
result_test_default_attribute := test_default_attributes.first()
assert result_test_default_attribute.name == 'Hitalo'
assert test_default_attribute.created_at or { '' } == ''
assert test_default_attribute.created_at1 or { '' } == ''
assert test_default_attribute.created_at2 or { '' } == ''
assert result_test_default_attribute.created_at or { '' }.len == 8 // HH:MM:SS
assert result_test_default_attribute.created_at1 or { '' }.len == 10 // YYYY-MM-DD
assert result_test_default_attribute.created_at2 or { '' }.len == 19 // YYYY-MM-DD HH:MM:SS
sql db {
drop table TestDefaultAttribute
}!
}
fn test_get_affected_rows_count() {
mut db := sqlite.connect(':memory:') or { panic(err) }
defer {
db.close() or { panic(err) }
}
db.exec('create table EntityToTest(
id integer not null constraint tbl_pk primary key,
smth integer
);')!
fst := EntityToTest{
id: 1
smth: '1'
}
sql db {
insert fst into EntityToTest
} or { panic('first insert failed') }
assert db.get_affected_rows_count() == 1
snd := EntityToTest{
id: 1
smth: '2'
}
mut sndfailed := false
sql db {
insert snd into EntityToTest
} or { sndfailed = true }
assert db.get_affected_rows_count() == 0
assert sndfailed
all := sql db {
select from EntityToTest
}!
assert 1 == all.len
sql db {
update EntityToTest set smth = '2' where id == 1
}!
assert db.get_affected_rows_count() == 1
sql db {
update EntityToTest set smth = '2' where id == 2
}!
assert db.get_affected_rows_count() == 0
sql db {
delete from EntityToTest where id == 2
}!
assert db.get_affected_rows_count() == 0
sql db {
delete from EntityToTest where id == 1
}!
assert db.get_affected_rows_count() == 1
}
fn test_sqlite_orm_supports_time_duration_alias_fields() {
mut db := sqlite.connect(':memory:') or { panic(err) }
defer {
db.close() or { panic(err) }
}
sql db {
create table TestDurationAlias
}!
result := db.exec('pragma table_info(TestDurationAlias);')!
assert result.len == 2
assert result[1].vals[1] == 'duration'
assert result[1].vals[2] == 'INTEGER'
record := TestDurationAlias{
duration: 3 * time.second
}
sql db {
insert record into TestDurationAlias
}!
rows := sql db {
select from TestDurationAlias limit 1
}!
assert rows.len == 1
assert rows[0].duration == 3 * time.second
}
fn test_sqlite_orm_tutorial_style_select_method() {
mut app := TutorialBlogApp{
db: sqlite.connect(':memory:') or { panic(err) }
}
defer {
app.db.close() or { panic(err) }
}
sql app.db {
create table TutorialBlogArticle
}!
first_article := TutorialBlogArticle{
title: 'Hello, world!'
text: 'V is great.'
}
second_article := TutorialBlogArticle{
title: 'Second post.'
text: 'Hm... what should I write about?'
}
sql app.db {
insert first_article into TutorialBlogArticle
insert second_article into TutorialBlogArticle
}!
articles := app.find_all_tutorial_blog_articles()
assert articles.len == 2
mut titles := articles.map(it.title)
titles.sort()
assert titles == ['Hello, world!', 'Second post.']
}
+204
View File
@@ -0,0 +1,204 @@
// vtest build: present_sqlite3?
import db.sqlite
import orm
import os
type Connection = sqlite.DB
struct User {
pub:
id int @[primary; sql: serial]
name string
last_name ?string
}
type Content = []u8 | string
struct Host {
pub mut:
db Connection
}
fn (back Host) get_users() []User {
return []
}
fn create_host(db Connection) !Host {
sql db {
create table User
}!
return Host{
db: db
}
}
fn test_sqlite() {
mut db := sqlite.connect(':memory:') or { panic(err) }
assert db.is_open
assert db.validate()!
db.exec('drop table if exists users')!
db.exec("create table users (id integer primary key, name text default '', last_name text null default null);")!
db.exec("insert into users (name) values ('Sam')")!
assert db.last_insert_rowid() == 1
assert db.get_affected_rows_count() == 1
db.exec("insert into users (name) values ('Peter')")!
assert db.last_insert_rowid() == 2
db.exec("insert into users (name) values ('Kate')")!
assert db.last_insert_rowid() == 3
db.exec_param('insert into users (name) values (?)', 'Tom')!
assert db.last_insert_rowid() == 4
nr_users := db.q_int('select count(*) from users')!
assert nr_users == 4
name := db.q_string('select name from users where id = 1')!
assert name == 'Sam'
username := db.exec_param('select name from users where id = ?', '1')!
assert username[0].vals[0] == 'Sam'
// this insert will be rejected due to duplicated id
db.exec("insert into users (id,name) values (1,'Silly')")!
assert db.get_affected_rows_count() == 0
mut users := db.exec('select * from users')!
// dump(users)
assert users.len == 4
code := db.exec_none('vacuum')
assert code == 101
user := db.exec_one('select * from users where id = 3') or { panic(err) }
// dump(user)
assert user.vals.len == 3
db.exec("update users set name='zzzz' where name='qqqq'")!
assert db.get_affected_rows_count() == 0
db.exec("update users set name='Peter1' where name='Peter'")!
assert db.get_affected_rows_count() == 1
db.exec_param_many('update users set name=? where name=?', ['Peter', 'Peter1'])!
assert db.get_affected_rows_count() == 1
db.exec("delete from users where name='qqqq'")!
assert db.get_affected_rows_count() == 0
db.exec("delete from users where name='Sam'")!
assert db.get_affected_rows_count() == 1
// transaction test
db.begin()!
db.exec("insert into users (name) values ('John')")!
assert db.last_insert_rowid() == 5
db.savepoint('new_savepoint')!
db.exec("insert into users (name) values ('Kitty')")!
assert db.last_insert_rowid() == 6
db.rollback_to('new_savepoint')!
db.exec("insert into users (name) values ('Mars')")!
assert db.last_insert_rowid() == 6
db.commit()!
users = db.exec('select * from users')!
// dump(users)
assert users.len == 5
db.close() or { panic(err) }
assert !db.is_open
}
fn test_can_access_sqlite_result_consts() {
assert sqlite.sqlite_ok == 0
assert sqlite.sqlite_error == 1
// assert sqlite.misuse == 21
assert sqlite.sqlite_row == 100
assert sqlite.sqlite_done == 101
}
fn test_alias_db() {
mut host := create_host(sqlite.connect(':memory:')!)!
host.db.close()!
assert true
}
fn test_exec_param_many() {
mut db := sqlite.connect(':memory:') or { panic(err) }
assert db.is_open
db.exec('drop table if exists users')!
db.exec("create table users (id integer primary key, name text default '' unique);")!
db.exec("insert into users (name) values ('Sam')")!
db.exec_param_many('insert into users (id, name) values (?, ?)', [
'60',
'Sam',
]) or {
assert err.code() == 19 // constraint failure
db.close()!
return
}
db.close()!
assert false
}
fn test_exec_param_many2() {
mut db := sqlite.connect(':memory:') or { panic(err) }
assert db.is_open
db.exec('drop table if exists users')!
db.exec("create table users (id integer primary key, name text default '' unique);")!
db.exec_param_many('insert into users (id, name) values (?, ?)', [
['60', 'Sam'],
['61', 'Foo'],
['62', 'Bar'],
])!
count := db.q_int('select count(*) from users')!
assert count == 3
db.close()!
}
fn test_tables() {
mut db := sqlite.connect(':memory:') or { panic(err) }
db.exec('create table alpha (id integer)')!
db.exec('create table beta (id integer)')!
tbl := db.tables()!
assert tbl == ['alpha', 'beta']
db.close()!
}
fn test_columns() {
mut db := sqlite.connect(':memory:') or { panic(err) }
db.exec('create table items (id integer primary key, name text, price real)')!
cols := db.columns('items')!
assert cols == ['id', 'name', 'price']
db.close()!
}
fn test_schema() {
mut db := sqlite.connect(':memory:') or { panic(err) }
db.exec('create table things (id integer primary key, label text)')!
s := db.schema('things')!
assert s.contains('CREATE TABLE things')
// empty table name returns all objects
all := db.schema('')!
assert all.contains('CREATE TABLE things')
db.close()!
}
fn test_db_size() {
tmp := os.join_path(os.temp_dir(), 'test_db_size.db')
defer {
os.rm(tmp) or {}
}
mut db := sqlite.connect(tmp) or { panic(err) }
db.exec('create table t (id integer)')!
sz := db.db_size()!
assert sz > 0
db.close()!
}
fn test_orm_transaction_interface() {
mut db := sqlite.connect(':memory:') or { panic(err) }
defer {
db.close() or {}
}
mut conn := orm.TransactionalConnection(db)
mut tx := orm.begin(mut conn)!
tx.transaction[int](fn (mut tx orm.Tx) !int {
return 1
})!
tx.commit()!
}
@@ -0,0 +1,319 @@
// vtest build: present_sqlite3?
import db.sqlite
import rand
const max_file_name_len = 256
fn test_vfs_register() {
org_default_vfs := sqlite.get_default_vfs()!
assert org_default_vfs.zName != 0
vfs_name := 'sometest'
mut vfs_descr := &sqlite.Sqlite3_vfs{
zName: vfs_name.str
iVersion: 2
}
if _ := sqlite.get_vfs(vfs_name) {
panic('expected that vfs is not known')
}
vfs_descr.register_as_nondefault() or { panic('vfs register failed ${err}') }
sqlite.get_vfs(vfs_name)!
now_default_vfs := sqlite.get_default_vfs()!
assert now_default_vfs.zName == org_default_vfs.zName
vfs_descr.unregister() or { panic('vfs unregister failed ${err}') }
if _ := sqlite.get_vfs(vfs_name) {
panic('vfs supposedly unregistered yet somehow still foundable')
}
}
// minimal vfs based on example https://www.sqlite.org/src/doc/trunk/src/test_demovfs.c
fn test_verify_vfs_is_actually_used() {
wrapped := sqlite.get_default_vfs()!
vfs_name := 'sometest'
mut vfs_state := &ExampleVfsState{
log: []string{cap: 100}
}
mut vfs_descr := &sqlite.Sqlite3_vfs{
iVersion: 2
szOsFile: int(sizeof(ExampleVfsOpenedFile))
mxPathname: max_file_name_len
zName: vfs_name.str
pAppData: vfs_state
xOpen: example_vfs_open
xDelete: example_vfs_delete
xAccess: example_vfs_access
xFullPathname: example_vfs_fullpathname
xDlOpen: wrapped.xDlOpen
xDlError: wrapped.xDlError
xDlSym: wrapped.xDlSym
xDlClose: wrapped.xDlClose
xRandomness: wrapped.xRandomness
xSleep: wrapped.xSleep
xCurrentTime: wrapped.xCurrentTime
xGetLastError: example_vfs_getlasterror
xCurrentTimeInt64: wrapped.xCurrentTimeInt64
}
vfs_descr.register_as_nondefault()!
// normally this would be written to disk
mut db := sqlite.connect_full('foo.db', [.readwrite, .create], vfs_name)!
assert ['fullpathname from=foo.db to=foo.db}', 'open temp?=false name=foo.db', 'read file=foo.db'] == vfs_state.log
vfs_state.log.clear()
db.close()!
assert ['close file=foo.db'] == vfs_state.log
}
struct ExampleVfsState {
mut:
log []string
}
struct ExampleVfsOpenedFile {
mut:
base sqlite.Sqlite3_file
name string
vfs_state &ExampleVfsState
}
fn to_vfsstate(t &sqlite.Sqlite3_vfs) &ExampleVfsState {
unsafe {
p := t.pAppData
if p == 0 {
assert false, 'p should not be 0'
}
return &ExampleVfsState(p)
}
}
fn to_vfsopenedfile(t &sqlite.Sqlite3_file) &ExampleVfsOpenedFile {
unsafe {
if t == 0 {
assert false, 't should not be 0'
}
return &ExampleVfsOpenedFile(t)
}
}
fn example_vfs_fullpathname(vfs &sqlite.Sqlite3_vfs, input &char, size_of_output int, output &char) int {
println('fullpathname called')
mut vfs_state := to_vfsstate(vfs)
from := unsafe { cstring_to_vstring(input) }
unsafe {
vmemcpy(output, input, from.len)
output[from.len] = u8(0)
}
result := unsafe { cstring_to_vstring(output) }
vfs_state.log << 'fullpathname from=${from} to=${result}}'
return sqlite.sqlite_ok
}
fn example_vfs_access(vfs &sqlite.Sqlite3_vfs, zPath &char, flags int, pResOut &int) int {
println('access called')
mut vfs_state := &ExampleVfsState{}
unsafe {
assert 0 != vfs.pAppData
vfs_state = &ExampleVfsState(vfs.pAppData)
}
vfs_state.log << 'accessed'
return sqlite.sqlite_ok
}
fn example_vfs_open(vfs &sqlite.Sqlite3_vfs, file_name_or_null_for_tempfile &char, vfs_opened_file &sqlite.Sqlite3_file,
in_flags int, out_flags &int) int {
println('open called')
mut is_temp := false
mut file_name := ''
unsafe {
if file_name_or_null_for_tempfile == nil {
is_temp = true
file_name = rand.uuid_v4()
} else {
file_name = cstring_to_vstring(file_name_or_null_for_tempfile)
}
}
mut vfs_state := to_vfsstate(vfs)
unsafe {
mut outp := to_vfsopenedfile(vfs_opened_file)
outp.base.pMethods = &sqlite.Sqlite3_io_methods{
iVersion: 1
xClose: example_vfsfile_close
xRead: example_vfsfile_read
xWrite: example_vfsfile_write
xTruncate: example_vfsfile_truncate
xSync: example_vfsfile_sync
xFileSize: example_vfsfile_size
xLock: example_vfsfile_lock
xUnlock: example_vfsfile_unlock
xCheckReservedLock: example_vfsfile_checkreservedlock
xFileControl: example_vfsfile_filecontrol
xSectorSize: example_vfsfile_sectorsize
xDeviceCharacteristics: example_vfsfile_devicecharacteristics
}
outp.name = file_name.clone()
outp.vfs_state = vfs_state
}
vfs_state.log << 'open temp?=${is_temp} name=${file_name}'
return sqlite.sqlite_ok
}
fn example_vfsfile_checkreservedlock(file &sqlite.Sqlite3_file, pResOut &int) int {
println('file checkreservedlock')
unsafe {
*pResOut = 0
}
return sqlite.sqlite_ok
}
fn example_vfsfile_filecontrol(file &sqlite.Sqlite3_file, op int, arg voidptr) int {
println('file filecontrol')
return 0
}
fn example_vfsfile_devicecharacteristics(file &sqlite.Sqlite3_file) int {
println('file devicecharacteristics')
return 0
}
fn example_vfsfile_size(file &sqlite.Sqlite3_file, result &i64) int {
println('file size')
return sqlite.sqlite_ok
}
fn example_vfsfile_read(file &sqlite.Sqlite3_file, output voidptr, amount int, offset i64) int {
println('file read')
assert amount > 0
unsafe {
mut vfsfile := to_vfsopenedfile(file)
vfsfile.vfs_state.log << 'read file=${vfsfile.name}'
}
unsafe {
vmemset(output, 0, amount)
}
return sqlite.sqlite_ioerr_short_read
}
fn example_vfsfile_truncate(file &sqlite.Sqlite3_file, size i64) int {
println('file truncate')
return sqlite.sqlite_ok
}
fn example_vfsfile_sectorsize(file &sqlite.Sqlite3_file) int {
println('file sectorsize')
return 0
}
fn example_vfsfile_sync(file &sqlite.Sqlite3_file, flags int) int {
println('file sync called')
return sqlite.sqlite_ok
}
fn example_vfsfile_lock(file &sqlite.Sqlite3_file, elock int) int {
println('file lock called')
return sqlite.sqlite_ok
}
fn example_vfsfile_unlock(file &sqlite.Sqlite3_file, elock int) int {
println('file unlock called')
return sqlite.sqlite_ok
}
fn example_vfsfile_write(file &sqlite.Sqlite3_file, buf voidptr, amount int, offset i64) int {
println('file write called')
return sqlite.sqlite_ok
}
fn example_vfsfile_close(file &sqlite.Sqlite3_file) int {
println('file close called')
unsafe {
mut vfsfile := to_vfsopenedfile(file)
vfsfile.vfs_state.log << 'close file=${vfsfile.name}'
}
return sqlite.sqlite_ok
}
fn example_vfs_delete(vfs &sqlite.Sqlite3_vfs, name &char, sync_dir int) int {
println('vfs delete called')
return sqlite.sqlite_ok
}
fn example_vfs_getlasterror(vfs &sqlite.Sqlite3_vfs, i int, o &char) int {
println('vfs getlasterror called')
unsafe {
*o = 0
}
return sqlite.sqlite_ok
}
////////////////////////////////////////////////
struct Human {
name string
age f32
}
fn check_connect_full_default_vfs(vfs_name string) ! {
mut db := sqlite.connect_full(':memory:', [.readwrite, .create, .fullmutex], '')!
sql db {
create table Human
}!
h := Human{'Bilbo', 56}
sql db {
insert h into Human
}!
res := sql db {
select from Human
}!
db.close()!
assert res.len == 1
assert res[0] == h
}
fn test_connect_full_default_vfs() {
// passing '' here as vfs_name should work everywhere, and it should be equivalent
// to 'unix' or 'win32', depending on the current system:
check_connect_full_default_vfs('')!
$if windows {
check_connect_full_default_vfs('win32')!
} $else {
check_connect_full_default_vfs('unix')!
}
}
+99
View File
@@ -0,0 +1,99 @@
module sqlite
fn C.sqlite3_bind_null(&C.sqlite3_stmt, i32) i32
fn C.sqlite3_bind_double(&C.sqlite3_stmt, i32, f64) i32
fn C.sqlite3_bind_int(&C.sqlite3_stmt, i32, i32) i32
fn C.sqlite3_bind_int64(&C.sqlite3_stmt, i32, i64) i32
fn C.sqlite3_bind_text(&C.sqlite3_stmt, i32, &char, i32, voidptr) i32
// Only for V ORM
fn (db &DB) init_stmt(query string) (&C.sqlite3_stmt, int) {
// println('init_stmt("${query}")')
stmt := &C.sqlite3_stmt(unsafe { nil })
err := C.sqlite3_prepare_v2(db.conn, &char(query.str), query.len, &stmt, 0)
return stmt, err
}
fn (db &DB) new_init_stmt(query string) !Stmt {
stmt, err := db.init_stmt(query)
if err != sqlite_ok {
return db.error_message(err, query)
}
return Stmt{stmt, db}
}
fn (stmt &Stmt) bind_null(idx int) int {
return C.sqlite3_bind_null(stmt.stmt, idx)
}
fn (stmt &Stmt) bind_int(idx int, v int) int {
return C.sqlite3_bind_int(stmt.stmt, idx, v)
}
fn (stmt &Stmt) bind_i64(idx int, v i64) int {
return C.sqlite3_bind_int64(stmt.stmt, idx, v)
}
fn (stmt &Stmt) bind_f64(idx int, v f64) int {
return C.sqlite3_bind_double(stmt.stmt, idx, v)
}
fn (stmt &Stmt) bind_text(idx int, s string) int {
return C.sqlite3_bind_text(stmt.stmt, idx, voidptr(s.str), s.len, 0)
}
fn (stmt &Stmt) get_int(idx int) ?int {
if C.sqlite3_column_type(stmt.stmt, idx) == C.SQLITE_NULL {
return none
} else {
return C.sqlite3_column_int(stmt.stmt, idx)
}
}
fn (stmt &Stmt) get_i64(idx int) ?i64 {
if C.sqlite3_column_type(stmt.stmt, idx) == C.SQLITE_NULL {
return none
} else {
return C.sqlite3_column_int64(stmt.stmt, idx)
}
}
fn (stmt &Stmt) get_f64(idx int) ?f64 {
if C.sqlite3_column_type(stmt.stmt, idx) == C.SQLITE_NULL {
return none
} else {
return C.sqlite3_column_double(stmt.stmt, idx)
}
}
fn (stmt &Stmt) get_text(idx int) ?string {
if C.sqlite3_column_type(stmt.stmt, idx) == C.SQLITE_NULL {
return none
} else {
b := &char(C.sqlite3_column_text(stmt.stmt, idx))
if b == &char(unsafe { nil }) {
return ''
}
l := C.sqlite3_column_bytes(stmt.stmt, idx)
return unsafe { b.vstring_with_len(l) }
}
}
fn (stmt &Stmt) get_count() int {
return C.sqlite3_column_count(stmt.stmt)
}
fn (stmt &Stmt) step() int {
return C.sqlite3_step(stmt.stmt)
}
fn (stmt &Stmt) orm_step(query string) ! {
res := stmt.step()
if res != sqlite_ok && res != sqlite_done && res != sqlite_row {
return stmt.db.error_message(res, query)
}
}
fn (stmt &Stmt) finalize() {
C.sqlite3_finalize(stmt.stmt)
}
+234
View File
@@ -0,0 +1,234 @@
module sqlite
type Sig1 = fn (&C.sqlite3_file, &i64) int // https://github.com/vlang/v/issues/16291
type Sig2 = fn (&Sqlite3_file, &int) int // https://github.com/vlang/v/issues/16291
pub type Sqlite3_file = C.sqlite3_file
pub type Sqlite3_vfs = C.sqlite3_vfs
// https://www.sqlite.org/c3ref/vfs.html
type Fn_sqlite3_syscall_ptr = fn ()
// Keep these callback signatures named so v2 does not need to parse qualified
// type names inside inline `fn (...)` struct fields.
type Sqlite3_file_op_fn = fn (&Sqlite3_file) int
type Sqlite3_file_rw_fn = fn (&Sqlite3_file, voidptr, int, i64) int
type Sqlite3_file_truncate_fn = fn (&Sqlite3_file, i64) int
type Sqlite3_file_flag_fn = fn (&Sqlite3_file, int) int
type Sqlite3_file_control_fn = fn (&Sqlite3_file, int, voidptr) int
type Sqlite3_file_shm_map_fn = fn (&Sqlite3_file, int, int, int, &voidptr) int
type Sqlite3_file_shm_lock_fn = fn (&Sqlite3_file, int, int, int) int
type Sqlite3_file_shm_barrier_fn = fn (&Sqlite3_file)
type Sqlite3_file_fetch_fn = fn (&Sqlite3_file, i64, int, &voidptr) int
type Sqlite3_file_unfetch_fn = fn (&Sqlite3_file, i64, voidptr) int
type Sqlite3_vfs_open_fn = fn (&Sqlite3_vfs, &char, &Sqlite3_file, int, &int) int
type Sqlite3_vfs_delete_fn = fn (&Sqlite3_vfs, &char, int) int
type Sqlite3_vfs_access_fn = fn (&Sqlite3_vfs, &char, int, &int) int
type Sqlite3_vfs_fullpathname_fn = fn (&Sqlite3_vfs, &char, int, &char) int
type Sqlite3_vfs_dlopen_fn = fn (&Sqlite3_vfs, &char) voidptr
type Sqlite3_vfs_dlerror_fn = fn (&Sqlite3_vfs, int, &char)
type Sqlite3_vfs_dlsym_fn = fn (&Sqlite3_vfs, voidptr, &char) voidptr
type Sqlite3_vfs_dlclose_fn = fn (&Sqlite3_vfs, voidptr)
type Sqlite3_vfs_randomness_fn = fn (&Sqlite3_vfs, int, &char) int
type Sqlite3_vfs_sleep_fn = fn (&Sqlite3_vfs, int) int
type Sqlite3_vfs_current_time_fn = fn (&Sqlite3_vfs, &f64) int
type Sqlite3_vfs_get_last_error_fn = fn (&Sqlite3_vfs, int, &char) int
type Sqlite3_vfs_current_time_i64_fn = fn (&Sqlite3_vfs, &i64) int
type Sqlite3_vfs_set_system_call_fn = fn (&Sqlite3_vfs, &char, Fn_sqlite3_syscall_ptr) int
type Sqlite3_vfs_get_system_call_fn = fn (&Sqlite3_vfs, &char) Fn_sqlite3_syscall_ptr
type Sqlite3_vfs_next_system_call_fn = fn (&Sqlite3_vfs, &char) &char
// https://www.sqlite.org/c3ref/file.html
pub struct C.sqlite3_file {
pub mut:
pMethods &C.sqlite3_io_methods // Methods for an open file
}
// https://www.sqlite.org/c3ref/io_methods.html
@[heap]
pub struct C.sqlite3_io_methods {
mut:
// version 1 and later fields
iVersion int
xClose Sqlite3_file_op_fn
xRead Sqlite3_file_rw_fn
xWrite Sqlite3_file_rw_fn
xTruncate Sqlite3_file_truncate_fn
xSync Sqlite3_file_flag_fn
xFileSize Sig1
xLock Sqlite3_file_flag_fn
xUnlock Sqlite3_file_flag_fn
xCheckReservedLock Sig2
xFileControl Sqlite3_file_control_fn
xSectorSize Sqlite3_file_op_fn
xDeviceCharacteristics Sqlite3_file_op_fn
// version 2 and later fields
xShmMap Sqlite3_file_shm_map_fn
xShmLock Sqlite3_file_shm_lock_fn
xShmBarrier Sqlite3_file_shm_barrier_fn
xShmUnmap Sqlite3_file_flag_fn
// version 3 and later fields
xFetch Sqlite3_file_fetch_fn
xUnfetch Sqlite3_file_unfetch_fn
}
pub type Sqlite3_io_methods = C.sqlite3_io_methods
@[heap]
pub struct C.sqlite3_vfs {
pub mut:
// version 1 and later fields
iVersion int // Structure version number (currently 3)
szOsFile int // Size of subclassed sqlite3_file
mxPathname int // Maximum file pathname length
pNext &Sqlite3_vfs // Next registered VFS
zName &char // Name of this virtual file system
pAppData voidptr // Pointer to application-specific data
xOpen Sqlite3_vfs_open_fn
xDelete Sqlite3_vfs_delete_fn
xAccess Sqlite3_vfs_access_fn
xFullPathname Sqlite3_vfs_fullpathname_fn
xDlOpen Sqlite3_vfs_dlopen_fn
xDlError Sqlite3_vfs_dlerror_fn
xDlSym Sqlite3_vfs_dlsym_fn // to fn accepting void and returning
xDlClose Sqlite3_vfs_dlclose_fn
xRandomness Sqlite3_vfs_randomness_fn
xSleep Sqlite3_vfs_sleep_fn
xCurrentTime Sqlite3_vfs_current_time_fn
xGetLastError Sqlite3_vfs_get_last_error_fn
// version two and later only fields
xCurrentTimeInt64 Sqlite3_vfs_current_time_i64_fn
// version three and later only fields
xSetSystemCall Sqlite3_vfs_set_system_call_fn
xGetSystemCall Sqlite3_vfs_get_system_call_fn
xNextSystemCall Sqlite3_vfs_next_system_call_fn
}
// https://www.sqlite.org/c3ref/vfs_find.html
fn C.sqlite3_vfs_find(&char) &C.sqlite3_vfs
fn C.sqlite3_vfs_register(&C.sqlite3_vfs, i32) i32
fn C.sqlite3_vfs_unregister(&C.sqlite3_vfs) i32
// get_vfs Requests sqlite to return instance of VFS with given name.
// when such vfs is not known, `none` is returned
pub fn get_vfs(name string) !&Sqlite3_vfs {
res := C.sqlite3_vfs_find(name.str)
if res != unsafe { nil } {
return res
}
return error('sqlite3_vfs_find returned 0')
}
// get_default_vfs Asks sqlite for default VFS instance
pub fn get_default_vfs() !&Sqlite3_vfs {
res := C.sqlite3_vfs_find(unsafe { nil })
if res != unsafe { nil } {
return res
}
return error('sqlite3_vfs_find(0) returned 0')
}
// register_as_nondefault Asks sqlite to register VFS passed in receiver argument as the known VFS.
// more info about VFS: https://www.sqlite.org/c3ref/vfs.html
// 'not TODOs' to prevent corruption: https://sqlite.org/howtocorrupt.html
// example VFS: https://www.sqlite.org/src/doc/trunk/src/test_demovfs.c
pub fn (mut v Sqlite3_vfs) register_as_nondefault() ! {
res := C.sqlite3_vfs_register(v, 0)
if sqlite_ok != res {
return error('sqlite3_vfs_register returned ${res}')
}
}
// unregister Requests sqlite to stop using VFS as passed in receiver argument
pub fn (mut v Sqlite3_vfs) unregister() ! {
res := C.sqlite3_vfs_unregister(v)
if sqlite_ok != res {
return error('sqlite3_vfs_unregister returned ${res}')
}
}
// https://www.sqlite.org/c3ref/open.html
fn C.sqlite3_open_v2(&char, &&C.sqlite3, i32, &char) i32
// https://www.sqlite.org/c3ref/c_open_autoproxy.html
pub enum OpenModeFlag {
readonly = 0x00000001
readwrite = 0x00000002
create = 0x00000004
uri = 0x00000040
memory = 0x00000080
nomutex = 0x00008000
fullmutex = 0x00010000
sharedcache = 0x00020000
privatecache = 0x00040000
exrescode = 0x02000000
nofollow = 0x01000000
}
// connect_full Opens connection to sqlite database. It gives more control than `open`.
// Flags give control over readonly and create decisions. Specific VFS can be chosen.
// Pass '' for vfs_name, if you want to use the default VFS for the current platform,
// (which is equivalent to 'win32' on Windows, and 'unix' on !Windows systems).
pub fn connect_full(path string, mode_flags []OpenModeFlag, vfs_name string) !DB {
db := &C.sqlite3(unsafe { nil })
path_cstr := sqlite_cstring(path)
defer {
unsafe { free(path_cstr) }
}
mut flags := 0
for flag in mode_flags {
flags = flags | int(flag)
}
mut pstr := unsafe { &char(0) }
if vfs_name != '' {
pstr = sqlite_cstring(vfs_name)
}
defer {
if vfs_name != '' {
unsafe { free(pstr) }
}
}
code := C.sqlite3_open_v2(path_cstr, &db, flags, pstr)
if code != 0 {
return &SQLError{
msg: unsafe { cstring_to_vstring(&char(C.sqlite3_errstr(code))) }
code: code
}
}
return DB{
conn: db
is_open: true
}
}