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