vlib: add a vlib/mssql module (#10280)

pull/10285/head^2
youyuanwu 2021-05-31 04:08:31 -07:00 committed by GitHub
parent 5aa4f622b6
commit 8990114b4b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 360 additions and 0 deletions

View File

@ -0,0 +1,47 @@
# SQL Server ODBC
* This is a V wrapper of SQL Server ODBC C/C++ library
## Dependencies
* ODBC C/C++ library
* Linux Install: https://docs.microsoft.com/en-us/sql/connect/odbc/linux-mac/installing-the-microsoft-odbc-driver-for-sql-server
* `msodbcsql17` and `unixodbc-dev` packages needed
* Windows Install: https://docs.microsoft.com/en-us/sql/connect/odbc/microsoft-odbc-driver-for-sql-server
## TODO
* Support Windows
* Support Mac
* ORM
## Usage
```v ignore
import mssql
fn test_example() ? {
// connect to server
config := mssql.Config{
driver: 'ODBC Driver 17 for SQL Server'
server: 'tcp:localhost'
uid: '<your username>'
pwd: '<your password>'
}
mut conn := mssql.Connection{}
conn.connect(config.get_conn_str()) ?
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
}
}
```

View File

@ -0,0 +1,6 @@
module mssql
#flag -lodbc
#include <sql.h>
#include <sqlext.h>

View File

@ -0,0 +1,5 @@
module mssql
// TODO: implement windows
// #flag windows -I@VEXEROOT/thirdparty/msodbcsql17
// #flag windows @VEXEROOT/thirdparty/msodbcsql17/lib64

View File

@ -0,0 +1,27 @@
module mssql
fn C.SQLAllocHandle(HandleType C.SQLSMALLINT, InputHandle C.SQLHANDLE, OutputHandle &C.SQLHANDLE) C.SQLRETURN
fn C.SQLSetEnvAttr(EnvironmentHandle C.SQLHENV, Attribute C.SQLINTEGER, Value C.SQLPOINTER, StringLength C.SQLINTEGER) C.SQLRETURN
fn C.SQLGetDiagRec(HandleType C.SQLSMALLINT, Handle C.SQLHANDLE, RecNumber C.SQLSMALLINT, Sqlstate &C.SQLCHAR, NativeError &C.SQLINTEGER, MessageText &C.SQLCHAR, BufferLength C.SQLSMALLINT, TextLength &C.SQLSMALLINT) C.SQLRETURN
fn C.SQLSetConnectAttr(ConnectionHandle C.SQLHDBC, Attribute C.SQLINTEGER, Value C.SQLPOINTER, StringLength C.SQLINTEGER) C.SQLRETURN
fn C.SQLDriverConnect(hdbc C.SQLHDBC, hwnd C.SQLHWND, szConnStrIn &C.SQLCHAR, cbConnStrIn C.SQLSMALLINT, szConnStrOut &C.SQLCHAR, cbConnStrOutMax C.SQLSMALLINT, pcbConnStrOut &C.SQLSMALLINT, fDriverCompletion C.SQLUSMALLINT) C.SQLRETURN
fn C.SQLDisconnect(ConnectionHandle C.SQLHDBC) C.SQLRETURN
fn C.SQLExecDirect(StatementHandle C.SQLHSTMT, StatementText &C.SQLCHAR, TextLength C.SQLINTEGER) C.SQLRETURN
fn C.SQLBindCol(StatementHandle C.SQLHSTMT, ColumnNumber C.SQLUSMALLINT, TargetType C.SQLSMALLINT, TargetValue C.SQLPOINTER, BufferLength C.SQLLEN, StrLen_or_Ind &C.SQLLEN) C.SQLRETURN
fn C.SQLFetch(StatementHandle C.SQLHSTMT) C.SQLRETURN
fn C.SQLFreeHandle(HandleType C.SQLSMALLINT, Handle C.SQLHANDLE) C.SQLRETURN
fn C.SQLNumResultCols(StatementHandle C.SQLHSTMT, ColumnCount &C.SQLSMALLINT) C.SQLRETURN
fn C.SQLColAttribute(StatementHandle C.SQLHSTMT, ColumnNumber C.SQLUSMALLINT, FieldIdentifier C.SQLUSMALLINT, CharacterAttribute C.SQLPOINTER, BufferLength C.SQLSMALLINT, StringLength C.SQLSMALLINT, NumericAttribute &C.SQLLEN) C.SQLRETURN
fn C.SQLRowCount(StatementHandle C.SQLHSTMT, RowCount &C.SQLLEN) C.SQLRETURN

View File

@ -0,0 +1,13 @@
module mssql
pub struct Config {
pub:
driver string
server string
uid string
pwd string
}
pub fn (cfg Config) get_conn_str() string {
return 'Driver=$cfg.driver;Server=$cfg.server;UID=$cfg.uid;PWD=$cfg.pwd'
}

122
vlib/mssql/mssql.v 100644
View File

@ -0,0 +1,122 @@
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 {
err_str += ':odbc=$(&sql_state[0]).vstring():$i:${int(native_error)}:$(&message_text[0]).vstring()\n'
}
}
}
return err_str
}

View File

@ -0,0 +1,13 @@
module mssql
pub struct Row {
pub mut:
vals []string
}
pub struct Result {
pub mut:
rows []Row
// the number of rows affected by sql statement
num_rows_affected int
}

View File

@ -0,0 +1,127 @@
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
indicators []C.SQLLEN
}
// 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, cap: h.column_count}
h.indicators = []C.SQLLEN{len: h.column_count, cap: 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, cap: 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, &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 << string(content)
}
} 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
}