From 64558df76488d72a712c84feb527797862bfeb79 Mon Sep 17 00:00:00 2001 From: yuyi Date: Fri, 13 Jan 2023 23:02:32 +0800 Subject: [PATCH] vlib: move the mysql/sqlite/pg/mssql modules under vlib/db (#16820) --- cmd/tools/vast/vast.v | 1 + cmd/tools/vtest-self.v | 16 +- doc/docs.md | 2 +- examples/database/mysql.v | 2 +- examples/database/orm.v | 6 +- examples/database/psql/customer.v | 2 +- examples/database/sqlite.v | 2 +- .../v_vweb_orm/src/main.v | 2 +- .../src/databases/config_databases_sqlite.v | 2 +- .../README.md | 2 +- .../code/blog/blog.v | 2 +- vlib/db/mssql/README.md | 69 ++++ vlib/db/mssql/_cdef_nix.c.v | 6 + vlib/db/mssql/_cdef_windows.c.v | 12 + vlib/db/mssql/_cdefs.c.v | 27 ++ vlib/db/mssql/config.v | 22 + vlib/db/mssql/mssql.v | 125 ++++++ vlib/db/mssql/result.v | 13 + vlib/db/mssql/stmt_handle.v | 127 ++++++ vlib/db/mysql/README.md | 39 ++ vlib/db/mysql/_cdefs.c.v | 103 +++++ vlib/db/mysql/_cdefs_nix.c.v | 11 + vlib/db/mysql/_cdefs_windows.c.v | 5 + vlib/db/mysql/consts.v | 13 + vlib/db/mysql/enums.v | 81 ++++ vlib/db/mysql/mysql.v | 250 +++++++++++ vlib/{ => db}/mysql/mysql_orm_test.v | 2 +- vlib/db/mysql/orm.v | 387 ++++++++++++++++++ vlib/db/mysql/result.v | 153 +++++++ vlib/db/mysql/stmt.c.v | 285 +++++++++++++ vlib/db/mysql/utils.v | 26 ++ vlib/db/pg/README.md | 52 +++ vlib/db/pg/compatibility.h | 16 + vlib/db/pg/oid.v | 171 ++++++++ vlib/db/pg/orm.v | 298 ++++++++++++++ vlib/db/pg/pg.v | 348 ++++++++++++++++ vlib/{ => db}/pg/pg_orm_test.v | 2 +- vlib/db/sqlite/README.md | 39 ++ vlib/db/sqlite/orm.v | 184 +++++++++ vlib/db/sqlite/result_code.v | 118 ++++++ vlib/db/sqlite/sqlite.v | 340 +++++++++++++++ vlib/{ => db}/sqlite/sqlite_orm_test.v | 2 +- vlib/{ => db}/sqlite/sqlite_test.v | 2 +- .../sqlite/sqlite_vfs_lowlevel_test.v | 2 +- vlib/db/sqlite/stmt.v | 78 ++++ vlib/db/sqlite/vfs_lowlevel.v | 171 ++++++++ vlib/mssql/README.md | 69 +--- vlib/mssql/z_deprecated.v | 3 + vlib/mysql/README.md | 41 +- vlib/mysql/z_deprecated.v | 3 + vlib/orm/README.md | 2 +- vlib/orm/orm_sql_or_blocks_test.v | 2 +- vlib/orm/orm_test.v | 6 +- vlib/pg/README.md | 52 +-- vlib/pg/z_deprecated.v | 3 + vlib/sqlite/README.md | 39 +- vlib/sqlite/sqlite.v | 8 +- vlib/sqlite/vfs_lowlevel.v | 6 +- vlib/sqlite/z_deprecated.v | 7 + vlib/v/ast/ast.v | 5 +- vlib/v/checker/orm.v | 2 +- vlib/v/checker/tests/orm_empty_struct.vv | 2 +- vlib/v/checker/tests/orm_no_default_value.vv | 2 +- vlib/v/checker/tests/orm_not_a_struct.vv | 2 +- .../orm_using_undefined_var_in_where_err.vv | 2 +- .../tests/struct_type_is_private_err.out | 12 +- .../tests/struct_type_is_private_err.vv | 4 +- vlib/v/fmt/tests/orm_keep.vv | 2 +- vlib/v/fmt/tests/orm_or_keep.vv | 2 +- vlib/v/gen/c/sql.v | 82 +--- vlib/v/tests/fn_literal_type_test.v | 2 +- vlib/v/tests/orm_joined_tables_select_test.v | 2 +- vlib/v/tests/orm_sub_array_struct_test.v | 2 +- vlib/v/tests/orm_sub_struct_test.v | 2 +- .../tests/sql_statement_inside_fn_call_test.v | 2 +- vlib/vweb/vweb_app_test.v | 2 +- 76 files changed, 3668 insertions(+), 320 deletions(-) create mode 100644 vlib/db/mssql/README.md create mode 100644 vlib/db/mssql/_cdef_nix.c.v create mode 100644 vlib/db/mssql/_cdef_windows.c.v create mode 100644 vlib/db/mssql/_cdefs.c.v create mode 100644 vlib/db/mssql/config.v create mode 100644 vlib/db/mssql/mssql.v create mode 100644 vlib/db/mssql/result.v create mode 100644 vlib/db/mssql/stmt_handle.v create mode 100644 vlib/db/mysql/README.md create mode 100644 vlib/db/mysql/_cdefs.c.v create mode 100644 vlib/db/mysql/_cdefs_nix.c.v create mode 100644 vlib/db/mysql/_cdefs_windows.c.v create mode 100644 vlib/db/mysql/consts.v create mode 100644 vlib/db/mysql/enums.v create mode 100644 vlib/db/mysql/mysql.v rename vlib/{ => db}/mysql/mysql_orm_test.v (99%) create mode 100644 vlib/db/mysql/orm.v create mode 100644 vlib/db/mysql/result.v create mode 100644 vlib/db/mysql/stmt.c.v create mode 100644 vlib/db/mysql/utils.v create mode 100644 vlib/db/pg/README.md create mode 100644 vlib/db/pg/compatibility.h create mode 100644 vlib/db/pg/oid.v create mode 100644 vlib/db/pg/orm.v create mode 100644 vlib/db/pg/pg.v rename vlib/{ => db}/pg/pg_orm_test.v (99%) create mode 100644 vlib/db/sqlite/README.md create mode 100644 vlib/db/sqlite/orm.v create mode 100644 vlib/db/sqlite/result_code.v create mode 100644 vlib/db/sqlite/sqlite.v rename vlib/{ => db}/sqlite/sqlite_orm_test.v (99%) rename vlib/{ => db}/sqlite/sqlite_test.v (99%) rename vlib/{ => db}/sqlite/sqlite_vfs_lowlevel_test.v (99%) create mode 100644 vlib/db/sqlite/stmt.v create mode 100644 vlib/db/sqlite/vfs_lowlevel.v create mode 100644 vlib/mssql/z_deprecated.v create mode 100644 vlib/mysql/z_deprecated.v create mode 100644 vlib/pg/z_deprecated.v create mode 100644 vlib/sqlite/z_deprecated.v diff --git a/cmd/tools/vast/vast.v b/cmd/tools/vast/vast.v index 1b83ef7445..6019e0fbb7 100644 --- a/cmd/tools/vast/vast.v +++ b/cmd/tools/vast/vast.v @@ -1752,6 +1752,7 @@ fn (t Tree) sql_expr(node ast.SqlExpr) &Node { fn (t Tree) sql_stmt(node ast.SqlStmt) &Node { mut obj := new_object() obj.add_terse('ast_type', t.string_node('SqlStmt')) + obj.add_terse('db_expr_type', t.type_node(node.db_expr_type)) obj.add_terse('db_expr', t.expr(node.db_expr)) obj.add_terse('or_expr', t.or_expr(node.or_expr)) obj.add('pos', t.pos(node.pos)) diff --git a/cmd/tools/vtest-self.v b/cmd/tools/vtest-self.v index 2c5f7ca27f..53ad78cd72 100644 --- a/cmd/tools/vtest-self.v +++ b/cmd/tools/vtest-self.v @@ -88,8 +88,8 @@ const ( 'cmd/tools/vdoc/tests/vdoc_file_test.v', /* fails on Windows; order of output is not as expected */ 'vlib/context/onecontext/onecontext_test.v', 'vlib/context/deadline_test.v' /* sometimes blocks */, - 'vlib/mysql/mysql_orm_test.v' /* mysql not installed */, - 'vlib/pg/pg_orm_test.v' /* pg not installed */, + 'vlib/db/mysql/mysql_orm_test.v' /* mysql not installed */, + 'vlib/db/pg/pg_orm_test.v' /* pg not installed */, ] skip_fsanitize_too_slow = [ // These tests are too slow to be run in the CI on each PR/commit @@ -120,9 +120,9 @@ const ( 'vlib/net/tcp_test.v', 'vlib/orm/orm_test.v', 'vlib/orm/orm_sql_or_blocks_test.v', - 'vlib/sqlite/sqlite_test.v', - 'vlib/sqlite/sqlite_orm_test.v', - 'vlib/sqlite/sqlite_vfs_lowlevel_test.v', + 'vlib/db/sqlite/sqlite_test.v', + 'vlib/db/sqlite/sqlite_orm_test.v', + 'vlib/db/sqlite/sqlite_vfs_lowlevel_test.v', 'vlib/v/tests/orm_sub_struct_test.v', 'vlib/v/tests/orm_sub_array_struct_test.v', 'vlib/v/tests/orm_joined_tables_select_test.v', @@ -170,9 +170,9 @@ const ( 'vlib/net/http/http_test.v', 'vlib/net/http/status_test.v', 'vlib/net/websocket/ws_test.v', - 'vlib/sqlite/sqlite_test.v', - 'vlib/sqlite/sqlite_orm_test.v', - 'vlib/sqlite/sqlite_vfs_lowlevel_test.v', + 'vlib/db/sqlite/sqlite_test.v', + 'vlib/db/sqlite/sqlite_orm_test.v', + 'vlib/db/sqlite/sqlite_vfs_lowlevel_test.v', 'vlib/orm/orm_test.v', 'vlib/orm/orm_sql_or_blocks_test.v', 'vlib/v/tests/orm_sub_struct_test.v', diff --git a/doc/docs.md b/doc/docs.md index 89b274486b..cb9b10110f 100644 --- a/doc/docs.md +++ b/doc/docs.md @@ -4715,7 +4715,7 @@ V's ORM provides a number of benefits: then manually construct objects from the parsed results.) ```v -import sqlite +import db.sqlite // sets a custom table name. Default is struct name (case-sensitive) [table: 'customers'] diff --git a/examples/database/mysql.v b/examples/database/mysql.v index 477c3ad09e..228c447381 100644 --- a/examples/database/mysql.v +++ b/examples/database/mysql.v @@ -1,4 +1,4 @@ -import mysql +import db.mysql fn main() { mut conn := mysql.Connection{ diff --git a/examples/database/orm.v b/examples/database/orm.v index 5081df6df2..affa5ab8b4 100644 --- a/examples/database/orm.v +++ b/examples/database/orm.v @@ -1,6 +1,6 @@ -import sqlite -import mysql -import pg +import db.sqlite +import db.mysql +import db.pg [table: 'modules'] struct Module { diff --git a/examples/database/psql/customer.v b/examples/database/psql/customer.v index 4538a94728..fc6ac0ab36 100644 --- a/examples/database/psql/customer.v +++ b/examples/database/psql/customer.v @@ -1,6 +1,6 @@ module main -import pg +import db.pg const dash = '----------------------------------------------------------------' diff --git a/examples/database/sqlite.v b/examples/database/sqlite.v index 52d5207a11..a97de551ff 100644 --- a/examples/database/sqlite.v +++ b/examples/database/sqlite.v @@ -1,4 +1,4 @@ -import sqlite +import db.sqlite fn main() { db := sqlite.connect(':memory:')! diff --git a/examples/js_dom_draw_bechmark_chart/v_vweb_orm/src/main.v b/examples/js_dom_draw_bechmark_chart/v_vweb_orm/src/main.v index 5e31bf963a..bf7d8b084b 100644 --- a/examples/js_dom_draw_bechmark_chart/v_vweb_orm/src/main.v +++ b/examples/js_dom_draw_bechmark_chart/v_vweb_orm/src/main.v @@ -2,7 +2,7 @@ module main import vweb import time -import sqlite +import db.sqlite struct App { vweb.Context diff --git a/examples/vweb_orm_jwt/src/databases/config_databases_sqlite.v b/examples/vweb_orm_jwt/src/databases/config_databases_sqlite.v index d06168c864..c76addc40a 100644 --- a/examples/vweb_orm_jwt/src/databases/config_databases_sqlite.v +++ b/examples/vweb_orm_jwt/src/databases/config_databases_sqlite.v @@ -1,6 +1,6 @@ module databases -import sqlite +import db.sqlite pub fn create_db_connection() !sqlite.DB { mut db := sqlite.connect('database.db')! diff --git a/tutorials/building_a_simple_web_blog_with_vweb/README.md b/tutorials/building_a_simple_web_blog_with_vweb/README.md index bf95e7f675..e0bed50baf 100644 --- a/tutorials/building_a_simple_web_blog_with_vweb/README.md +++ b/tutorials/building_a_simple_web_blog_with_vweb/README.md @@ -182,7 +182,7 @@ Add a SQLite handle to `App`: ```v oksyntax // blog.v -import sqlite +import db.sqlite import vweb struct App { diff --git a/tutorials/building_a_simple_web_blog_with_vweb/code/blog/blog.v b/tutorials/building_a_simple_web_blog_with_vweb/code/blog/blog.v index 7e80713b72..b0d5c0c4d8 100644 --- a/tutorials/building_a_simple_web_blog_with_vweb/code/blog/blog.v +++ b/tutorials/building_a_simple_web_blog_with_vweb/code/blog/blog.v @@ -2,7 +2,7 @@ module main import vweb import time -import sqlite +import db.sqlite import json struct App { diff --git a/vlib/db/mssql/README.md b/vlib/db/mssql/README.md new file mode 100644 index 0000000000..80f8181766 --- /dev/null +++ b/vlib/db/mssql/README.md @@ -0,0 +1,69 @@ +# SQL Server ODBC + +* This is a V wrapper of SQL Server ODBC C/C++ library + +## Dependencies +* ODBC C/C++ library + * Linux Install: + * Details: https://docs.microsoft.com/en-us/sql/connect/odbc/linux-mac/installing-the-microsoft-odbc-driver-for-sql-server + * `msodbcsql17` and `unixodbc-dev` packages are needed + * Windows Install: + * `odbc` lib is included in windows sdk for most of distributions, + so there is no need to install it separately + * Details: https://docs.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 Mac +* Support 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: '' + pwd: '' + } + + 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 + } +} +``` diff --git a/vlib/db/mssql/_cdef_nix.c.v b/vlib/db/mssql/_cdef_nix.c.v new file mode 100644 index 0000000000..0a9ec00ea9 --- /dev/null +++ b/vlib/db/mssql/_cdef_nix.c.v @@ -0,0 +1,6 @@ +module mssql + +#flag -lodbc + +#include +#include diff --git a/vlib/db/mssql/_cdef_windows.c.v b/vlib/db/mssql/_cdef_windows.c.v new file mode 100644 index 0000000000..61724eecc0 --- /dev/null +++ b/vlib/db/mssql/_cdef_windows.c.v @@ -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 diff --git a/vlib/db/mssql/_cdefs.c.v b/vlib/db/mssql/_cdefs.c.v new file mode 100644 index 0000000000..7ce0d08da6 --- /dev/null +++ b/vlib/db/mssql/_cdefs.c.v @@ -0,0 +1,27 @@ +module mssql + +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 diff --git a/vlib/db/mssql/config.v b/vlib/db/mssql/config.v new file mode 100644 index 0000000000..09652e436d --- /dev/null +++ b/vlib/db/mssql/config.v @@ -0,0 +1,22 @@ +module mssql + +// Config TODO +pub struct Config { +pub: + driver string + server string + uid string + pwd 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 +} + +// get_conn_str TODO +pub fn (cfg Config) get_conn_str() string { + mut str := 'Driver=${cfg.driver};Server=${cfg.server};UID=${cfg.uid};PWD=${cfg.pwd}' + if cfg.dbname != '' { + str += ';Database=${cfg.dbname}' + } + return str +} diff --git a/vlib/db/mssql/mssql.v b/vlib/db/mssql/mssql.v new file mode 100644 index 0000000000..f0d9ea3c64 --- /dev/null +++ b/vlib/db/mssql/mssql.v @@ -0,0 +1,125 @@ +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 +} diff --git a/vlib/db/mssql/result.v b/vlib/db/mssql/result.v new file mode 100644 index 0000000000..f5483a2858 --- /dev/null +++ b/vlib/db/mssql/result.v @@ -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 +} diff --git a/vlib/db/mssql/stmt_handle.v b/vlib/db/mssql/stmt_handle.v new file mode 100644 index 0000000000..e229c8aca1 --- /dev/null +++ b/vlib/db/mssql/stmt_handle.v @@ -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} + h.indicators = []C.SQLLEN{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, &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 +} diff --git a/vlib/db/mysql/README.md b/vlib/db/mysql/README.md new file mode 100644 index 0000000000..e5a01b4d42 --- /dev/null +++ b/vlib/db/mysql/README.md @@ -0,0 +1,39 @@ +For Linux, you need to install `MySQL development` package and `pkg-config`. + +For Windows, install [the installer](https://dev.mysql.com/downloads/installer/) , +then copy the `include` and `lib` folders to `\thirdparty\mysql`. + +Note: if you encounter weird errors (your program just exits right away, without +printing any messages, even though you have `println('hi')` statements in your +`fn main()`), when trying to run a program that does `import db.mysql` on windows, you +may need to copy the .dll file: `thirdparty/mysql/lib/libmysql.dll` , into the folder +of the executable too (it should be right next to the .exe file). +This is a temporary workaround, until we have a more permanent solution, or at least +more user friendly errors for that situation. + +## Basic Usage + +```v oksyntax +import db.mysql + +// Create connection +mut connection := mysql.Connection{ + username: 'root' + dbname: 'mysql' +} +// Connect to server +connection.connect()? +// Change the default database +connection.select_db('db_users')? +// Do a query +get_users_query_result := connection.query('SELECT * FROM users')? +// Get the result as maps +for user in get_users_query_result.maps() { + // Access the name of user + println(user['name']) +} +// Free the query result +get_users_query_result.free() +// Close the connection if needed +connection.close() +``` diff --git a/vlib/db/mysql/_cdefs.c.v b/vlib/db/mysql/_cdefs.c.v new file mode 100644 index 0000000000..9c69d33a28 --- /dev/null +++ b/vlib/db/mysql/_cdefs.c.v @@ -0,0 +1,103 @@ +module mysql + +[typedef] +struct C.MYSQL { +} + +[typedef] +struct C.MYSQL_RES { +} + +[typedef] +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 // Database for table + 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 // Div flags + decimals u32 // Number of decimals in field + charsetnr u32 // Character set + @type int // Type of field. See mysql_com.h for types +} + +fn C.mysql_init(mysql &C.MYSQL) &C.MYSQL + +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 + +fn C.mysql_query(mysql &C.MYSQL, q &u8) int + +fn C.mysql_use_result(mysql &C.MYSQL) + +fn C.mysql_real_query(mysql &C.MYSQL, q &u8, len u32) int + +fn C.mysql_select_db(mysql &C.MYSQL, db &u8) int + +fn C.mysql_change_user(mysql &C.MYSQL, user &u8, password &u8, db &u8) bool + +fn C.mysql_affected_rows(mysql &C.MYSQL) u64 + +fn C.mysql_options(mysql &C.MYSQL, option int, arg voidptr) int + +fn C.mysql_get_option(mysql &C.MYSQL, option int, arg voidptr) int + +fn C.mysql_list_tables(mysql &C.MYSQL, wild &u8) &C.MYSQL_RES + +fn C.mysql_num_fields(res &C.MYSQL_RES) int + +fn C.mysql_num_rows(res &C.MYSQL_RES) u64 + +fn C.mysql_autocommit(mysql &C.MYSQL, mode bool) + +fn C.mysql_refresh(mysql &C.MYSQL, options u32) int + +fn C.mysql_reset_connection(mysql &C.MYSQL) int + +fn C.mysql_ping(mysql &C.MYSQL) int + +fn C.mysql_store_result(mysql &C.MYSQL) &C.MYSQL_RES + +fn C.mysql_fetch_row(res &C.MYSQL_RES) &&u8 + +fn C.mysql_fetch_fields(res &C.MYSQL_RES) &C.MYSQL_FIELD + +fn C.mysql_free_result(res &C.MYSQL_RES) + +fn C.mysql_real_escape_string(mysql &C.MYSQL, to &u8, from &u8, len u64) u64 + +// fn C.mysql_real_escape_string_quote(mysql &C.MYSQL, to &byte, from &byte, len u64, quote byte) u64 (Don't exist in mariadb) + +fn C.mysql_close(sock &C.MYSQL) + +// INFO & VERSION +fn C.mysql_info(mysql &C.MYSQL) &u8 + +fn C.mysql_get_host_info(mysql &C.MYSQL) &u8 + +fn C.mysql_get_server_info(mysql &C.MYSQL) &u8 + +fn C.mysql_get_server_version(mysql &C.MYSQL) u64 + +fn C.mysql_get_client_version() u64 + +fn C.mysql_get_client_info() &u8 + +// DEBUG & ERROR INFO +fn C.mysql_error(mysql &C.MYSQL) &u8 + +fn C.mysql_errno(mysql &C.MYSQL) int + +fn C.mysql_dump_debug_info(mysql &C.MYSQL) int + +fn C.mysql_debug(debug &u8) diff --git a/vlib/db/mysql/_cdefs_nix.c.v b/vlib/db/mysql/_cdefs_nix.c.v new file mode 100644 index 0000000000..1f37210529 --- /dev/null +++ b/vlib/db/mysql/_cdefs_nix.c.v @@ -0,0 +1,11 @@ +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 # Please install the libmysqlclient-dev development headers +} $else { + #pkgconfig mariadb + #include # Please install the libmariadb-dev development headers +} diff --git a/vlib/db/mysql/_cdefs_windows.c.v b/vlib/db/mysql/_cdefs_windows.c.v new file mode 100644 index 0000000000..9e186966ed --- /dev/null +++ b/vlib/db/mysql/_cdefs_windows.c.v @@ -0,0 +1,5 @@ +module mysql + +#flag windows -I@VEXEROOT/thirdparty/mysql/include +#flag windows @VEXEROOT/thirdparty/mysql/lib/libmysql.dll +#include # Please install https://dev.mysql.com/downloads/installer/ , then put the include/ and lib/ folders in thirdparty/mysql diff --git a/vlib/db/mysql/consts.v b/vlib/db/mysql/consts.v new file mode 100644 index 0000000000..2e9962a335 --- /dev/null +++ b/vlib/db/mysql/consts.v @@ -0,0 +1,13 @@ +module mysql + +// MYSQL REFRESH FLAGS +pub const ( + refresh_grant = u32(C.REFRESH_GRANT) + refresh_log = u32(C.REFRESH_LOG) + refresh_tables = u32(C.REFRESH_TABLES) + refresh_hosts = u32(C.REFRESH_HOSTS) + refresh_status = u32(C.REFRESH_STATUS) + refresh_threads = u32(C.REFRESH_THREADS) + refresh_slave = u32(C.REFRESH_SLAVE) + refresh_master = u32(C.REFRESH_MASTER) +) diff --git a/vlib/db/mysql/enums.v b/vlib/db/mysql/enums.v new file mode 100644 index 0000000000..0aa86f4f1e --- /dev/null +++ b/vlib/db/mysql/enums.v @@ -0,0 +1,81 @@ +module mysql + +// 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` +pub fn (f FieldType) get_len() u32 { + return match f { + .type_blob { 262140 } + else { 0 } + } +} diff --git a/vlib/db/mysql/mysql.v b/vlib/db/mysql/mysql.v new file mode 100644 index 0000000000..b03e8a0b6f --- /dev/null +++ b/vlib/db/mysql/mysql.v @@ -0,0 +1,250 @@ +module mysql + +// Values for the capabilities flag bitmask used by the MySQL protocol. +// See more on https://dev.mysql.com/doc/dev/mysql-server/latest/group__group__cs__capabilities__flags.html#details +pub enum ConnectionFlag { + // CAN_HANDLE_EXPIRED_PASSWORDS = C.CAN_HANDLE_EXPIRED_PASSWORDS + client_compress = C.CLIENT_COMPRESS + client_found_rows = C.CLIENT_FOUND_ROWS + client_ignore_sigpipe = C.CLIENT_IGNORE_SIGPIPE + client_ignore_space = C.CLIENT_IGNORE_SPACE + client_interactive = C.CLIENT_INTERACTIVE + client_local_files = C.CLIENT_LOCAL_FILES + client_multi_results = C.CLIENT_MULTI_RESULTS + client_multi_statements = C.CLIENT_MULTI_STATEMENTS + client_no_schema = C.CLIENT_NO_SCHEMA + client_odbc = C.CLIENT_ODBC + // client_optional_resultset_metadata = C.CLIENT_OPTIONAL_RESULTSET_METADATA + client_ssl = C.CLIENT_SSL + client_remember_options = C.CLIENT_REMEMBER_OPTIONS +} + +struct SQLError { + MessageError +} + +// TODO: Documentation +pub struct Connection { +mut: + conn &C.MYSQL = C.mysql_init(0) +pub mut: + host string = '127.0.0.1' + port u32 = 3306 + username string + password string + dbname string + flag ConnectionFlag +} + +// connect - create a new connection to the MySQL server. +pub fn (mut conn Connection) connect() !bool { + instance := C.mysql_init(conn.conn) + conn.conn = C.mysql_real_connect(instance, conn.host.str, conn.username.str, conn.password.str, + conn.dbname.str, conn.port, 0, conn.flag) + if isnil(conn.conn) { + return error_with_code(get_error_msg(instance), get_errno(instance)) + } + return true +} + +// query - make an SQL query and receive the results. +// `query()` cannot be used for statements that contain binary data; +// Use `real_query()` instead. +pub fn (conn Connection) query(q string) !Result { + if C.mysql_query(conn.conn, q.str) != 0 { + return error_with_code(get_error_msg(conn.conn), get_errno(conn.conn)) + } + res := C.mysql_store_result(conn.conn) + return Result{res} +} + +// use_result - reads the result of a query +// used after invoking mysql_real_query() or mysql_query(), +// for every statement that successfully produces a result set +// (SELECT, SHOW, DESCRIBE, EXPLAIN, CHECK TABLE, and so forth). +// This reads the result of a query directly from the server +// without storing it in a temporary table or local buffer, +// mysql_use_result is faster and uses much less memory than C.mysql_store_result(). +// You must mysql_free_result() after you are done with the result set. +pub fn (conn Connection) use_result() { + C.mysql_use_result(conn.conn) +} + +// real_query - make an SQL query and receive the results. +// `real_query()` can be used for statements containing binary data. +// (Binary data may contain the `\0` character, which `query()` +// interprets as the end of the statement string). In addition, +// `real_query()` is faster than `query()`. +pub fn (mut conn Connection) real_query(q string) !Result { + if C.mysql_real_query(conn.conn, q.str, q.len) != 0 { + return error_with_code(get_error_msg(conn.conn), get_errno(conn.conn)) + } + res := C.mysql_store_result(conn.conn) + return Result{res} +} + +// select_db - change the default database for database queries. +pub fn (mut conn Connection) select_db(dbname string) !bool { + if C.mysql_select_db(conn.conn, dbname.str) != 0 { + return error_with_code(get_error_msg(conn.conn), get_errno(conn.conn)) + } + return true +} + +// change_user - change the mysql user for the connection. +// Passing an empty string for the `dbname` parameter, resultsg in only changing +// the user and not changing the default database for the connection. +pub fn (mut conn Connection) change_user(username string, password string, dbname string) !bool { + mut ret := true + if dbname != '' { + ret = C.mysql_change_user(conn.conn, username.str, password.str, dbname.str) + } else { + ret = C.mysql_change_user(conn.conn, username.str, password.str, 0) + } + if !ret { + return error_with_code(get_error_msg(conn.conn), get_errno(conn.conn)) + } + return ret +} + +// affected_rows - return the number of rows changed/deleted/inserted +// by the last `UPDATE`, `DELETE`, or `INSERT` query. +pub fn (conn &Connection) affected_rows() u64 { + return C.mysql_affected_rows(conn.conn) +} + +// autocommit - turns on/off the auto-committing mode for the connection. +// When it is on, then each query is commited right away. +pub fn (mut conn Connection) autocommit(mode bool) { + C.mysql_autocommit(conn.conn, mode) +} + +// tables - returns a list of the names of the tables in the current database, +// that match the simple regular expression specified by the `wildcard` parameter. +// The `wildcard` parameter may contain the wildcard characters `%` or `_`. +// If an empty string is passed, it will return all tables. +// Calling `tables()` is similar to executing query `SHOW TABLES [LIKE wildcard]`. +pub fn (conn &Connection) tables(wildcard string) ![]string { + cres := C.mysql_list_tables(conn.conn, wildcard.str) + if isnil(cres) { + return error_with_code(get_error_msg(conn.conn), get_errno(conn.conn)) + } + res := Result{cres} + mut tables := []string{} + for row in res.rows() { + tables << row.vals[0] + } + return tables +} + +// escape_string - creates a legal SQL string for use in an SQL statement. +// The `s` argument is encoded to produce an escaped SQL string, +// taking into account the current character set of the connection. +pub fn (conn &Connection) escape_string(s string) string { + unsafe { + to := malloc_noscan(2 * s.len + 1) + C.mysql_real_escape_string(conn.conn, to, s.str, s.len) + return to.vstring() + } +} + +// set_option - sets extra connect options that affect the behavior of +// a connection. This function may be called multiple times to set several +// options. To retrieve the current values for an option, use `get_option()`. +pub fn (mut conn Connection) set_option(option_type int, val voidptr) { + C.mysql_options(conn.conn, option_type, val) +} + +// get_option - return the value of an option, settable by `set_option`. +// https://dev.mysql.com/doc/c-api/5.7/en/mysql-get-option.html +pub fn (conn &Connection) get_option(option_type int) !voidptr { + ret := unsafe { nil } + if C.mysql_get_option(conn.conn, option_type, &ret) != 0 { + return error_with_code(get_error_msg(conn.conn), get_errno(conn.conn)) + } + return ret +} + +// refresh - flush the tables or caches, or resets replication server +// information. The connected user must have the `RELOAD` privilege. +pub fn (mut conn Connection) refresh(options u32) !bool { + if C.mysql_refresh(conn.conn, options) != 0 { + return error_with_code(get_error_msg(conn.conn), get_errno(conn.conn)) + } + return true +} + +// reset - resets the connection, and clear the session state. +pub fn (mut conn Connection) reset() !bool { + if C.mysql_reset_connection(conn.conn) != 0 { + return error_with_code(get_error_msg(conn.conn), get_errno(conn.conn)) + } + return true +} + +// ping - pings a server connection, or tries to reconnect if the connection +// has gone down. +pub fn (mut conn Connection) ping() !bool { + if C.mysql_ping(conn.conn) != 0 { + return error_with_code(get_error_msg(conn.conn), get_errno(conn.conn)) + } + return true +} + +// close - closes the connection. +pub fn (mut conn Connection) close() { + C.mysql_close(conn.conn) +} + +// info - returns information about the most recently executed query. +// See more on https://dev.mysql.com/doc/c-api/8.0/en/mysql-info.html +pub fn (conn &Connection) info() string { + return resolve_nil_str(C.mysql_info(conn.conn)) +} + +// get_host_info - returns a string describing the type of connection in use, +// including the server host name. +pub fn (conn &Connection) get_host_info() string { + return unsafe { C.mysql_get_host_info(conn.conn).vstring() } +} + +// get_server_info - returns a string representing the MySQL server version. +// For example, `8.0.24`. +pub fn (conn &Connection) get_server_info() string { + return unsafe { C.mysql_get_server_info(conn.conn).vstring() } +} + +// get_server_version - returns an integer, representing 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. For example, `8.0.24` is returned as `80024`. +pub fn (conn &Connection) get_server_version() u64 { + return C.mysql_get_server_version(conn.conn) +} + +// dump_debug_info - instructs the server to write debugging information +// to the error log. The connected user must have the `SUPER` privilege. +pub fn (mut conn Connection) dump_debug_info() !bool { + if C.mysql_dump_debug_info(conn.conn) != 0 { + return error_with_code(get_error_msg(conn.conn), get_errno(conn.conn)) + } + return true +} + +// get_client_info - returns client version information as a string. +pub fn get_client_info() string { + return unsafe { C.mysql_get_client_info().vstring() } +} + +// get_client_version - returns the client version information as an integer. +pub fn get_client_version() u64 { + return C.mysql_get_client_version() +} + +// debug - does a `DBUG_PUSH` with the given string. +// `debug()` uses the Fred Fish debug library. +// To use this function, you must compile the client library to support debugging. +// See https://dev.mysql.com/doc/c-api/8.0/en/mysql-debug.html +pub fn debug(debug string) { + C.mysql_debug(debug.str) +} diff --git a/vlib/mysql/mysql_orm_test.v b/vlib/db/mysql/mysql_orm_test.v similarity index 99% rename from vlib/mysql/mysql_orm_test.v rename to vlib/db/mysql/mysql_orm_test.v index 0646728a80..5087ee3643 100644 --- a/vlib/mysql/mysql_orm_test.v +++ b/vlib/db/mysql/mysql_orm_test.v @@ -1,5 +1,5 @@ import orm -import mysql +import db.mysql import time struct TestCustomSqlType { diff --git a/vlib/db/mysql/orm.v b/vlib/db/mysql/orm.v new file mode 100644 index 0000000000..5753a09031 --- /dev/null +++ b/vlib/db/mysql/orm.v @@ -0,0 +1,387 @@ +module mysql + +import orm +import time + +type Prims = f32 | f64 | i16 | i64 | i8 | int | string | u16 | u32 | u64 | u8 + +// sql expr + +// @select is used internally by V's ORM for processing `SELECT ` queries +pub fn (db Connection) @select(config orm.SelectConfig, data orm.QueryData, where orm.QueryData) ![][]orm.Primitive { + query := orm.orm_select_gen(config, '`', false, '?', 0, where) + mut ret := [][]orm.Primitive{} + mut stmt := db.init_stmt(query) + stmt.prepare()! + + mysql_stmt_binder(mut stmt, where)! + mysql_stmt_binder(mut stmt, data)! + + if data.data.len > 0 || where.data.len > 0 { + stmt.bind_params()! + } + + mut status := stmt.execute()! + num_fields := stmt.get_field_count() + metadata := stmt.gen_metadata() + fields := stmt.fetch_fields(metadata) + mut dataptr := []&u8{} + + for i in 0 .. num_fields { + f := unsafe { fields[i] } + match unsafe { FieldType(f.@type) } { + .type_tiny { + dataptr << unsafe { malloc(1) } + } + .type_short { + dataptr << unsafe { malloc(2) } + } + .type_long { + dataptr << unsafe { malloc(4) } + } + .type_longlong { + dataptr << unsafe { malloc(8) } + } + .type_float { + dataptr << unsafe { malloc(4) } + } + .type_double { + dataptr << unsafe { malloc(8) } + } + .type_time, .type_date, .type_datetime, .type_time2, .type_datetime2 { + dataptr << unsafe { malloc(sizeof(C.MYSQL_TIME)) } + } + .type_string, .type_blob { + dataptr << unsafe { malloc(512) } + } + .type_var_string { + dataptr << unsafe { malloc(2) } + } + else { + return error('\'${unsafe { FieldType(f.@type) }}\' is not yet implemented. Please create a new issue at https://github.com/vlang/v/issues/new') + } + } + } + + lens := []u32{len: int(num_fields), init: 0} + stmt.bind_res(fields, dataptr, lens, num_fields) + + mut row := 0 + mut types := config.types.clone() + mut field_types := []FieldType{} + if config.is_count { + types = [orm.type_idx['u64']] + } + + for i, mut mysql_bind in stmt.res { + f := unsafe { fields[i] } + field_types << unsafe { FieldType(f.@type) } + match types[i] { + orm.type_string { + mysql_bind.buffer_type = C.MYSQL_TYPE_BLOB + mysql_bind.buffer_length = FieldType.type_blob.get_len() + } + orm.time { + match unsafe { FieldType(f.@type) } { + .type_long { + mysql_bind.buffer_type = C.MYSQL_TYPE_LONG + } + .type_time, .type_date, .type_datetime { + mysql_bind.buffer_type = C.MYSQL_TYPE_BLOB + mysql_bind.buffer_length = FieldType.type_blob.get_len() + } + .type_string, .type_blob {} + else { + return error('Unknown type ${f.@type}') + } + } + } + else {} + } + } + + stmt.bind_result_buffer()! + stmt.store_result()! + for { + status = stmt.fetch_stmt()! + + if status == 1 || status == 100 { + break + } + row++ + + data_list := buffer_to_primitive(dataptr, types, field_types)! + ret << data_list + } + + stmt.close()! + + return ret +} + +// sql stmt + +// insert is used internally by V's ORM for processing `INSERT ` queries +pub fn (db Connection) insert(table string, data orm.QueryData) ! { + mut converted_primitive_array := db.factory_orm_primitive_converted_from_sql(table, + data)! + + converted_primitive_data := orm.QueryData{ + fields: data.fields + data: converted_primitive_array + types: [] + kinds: [] + is_and: [] + } + + query, converted_data := orm.orm_stmt_gen(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 Connection) update(table string, data orm.QueryData, where orm.QueryData) ! { + query, _ := orm.orm_stmt_gen(table, '`', .update, false, '?', 1, data, where) + mysql_stmt_worker(db, query, data, where)! +} + +// delete is used internally by V's ORM for processing `DELETE ` queries +pub fn (db Connection) delete(table string, where orm.QueryData) ! { + query, _ := orm.orm_stmt_gen(table, '`', .delete, false, '?', 1, orm.QueryData{}, + where) + mysql_stmt_worker(db, query, orm.QueryData{}, where)! +} + +// last_id is used internally by V's ORM for post-processing `INSERT ` queries +pub fn (db Connection) last_id() orm.Primitive { + query := 'SELECT last_insert_id();' + id := db.query(query) or { + Result{ + result: 0 + } + } + return orm.Primitive(id.rows()[0].vals[0].int()) +} + +// DDL (table creation/destroying etc) + +// create is used internally by V's ORM for processing table creation queries (DDL) +pub fn (db Connection) create(table string, fields []orm.TableField) ! { + query := orm.orm_table_gen(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 Connection) drop(table string) ! { + query := 'DROP TABLE `${table}`;' + mysql_stmt_worker(db, query, orm.QueryData{}, orm.QueryData{})! +} + +fn mysql_stmt_worker(db Connection, query string, data orm.QueryData, where orm.QueryData) ! { + mut stmt := db.init_stmt(query) + stmt.prepare()! + mysql_stmt_binder(mut stmt, data)! + mysql_stmt_binder(mut stmt, where)! + if data.data.len > 0 || where.data.len > 0 { + stmt.bind_params()! + } + stmt.execute()! + stmt.close()! +} + +fn mysql_stmt_binder(mut stmt Stmt, d orm.QueryData) ! { + for data in d.data { + stmt_binder_match(mut stmt, data) + } +} + +fn stmt_binder_match(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_binder_match(mut stmt, unix) + } + orm.InfixType { + stmt_binder_match(mut stmt, data.right) + } + } +} + +fn buffer_to_primitive(data_list []&u8, types []int, field_types []FieldType) ![]orm.Primitive { + mut res := []orm.Primitive{} + + for i, data in data_list { + mut primitive := orm.Primitive(0) + 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 { + string_time := unsafe { cstring_to_vstring(&char(data)) } + primitive = time.parse(string_time)! + } + else {} + } + } + else { + return error('Unknown type ${types[i]}') + } + } + res << primitive + } + + return res +} + +fn mysql_type_from_v(typ int) !string { + str := 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'] { + 'BIGINT' + } + orm.type_idx['f32'] { + 'FLOAT' + } + orm.type_idx['f64'] { + 'DOUBLE' + } + orm.type_string { + 'TEXT' + } + orm.serial { + 'SERIAL' + } + orm.type_idx['bool'] { + 'BOOLEAN' + } + else { + '' + } + } + if str == '' { + return error('Unknown type ${typ}') + } + return str +} + +fn (db Connection) factory_orm_primitive_converted_from_sql(table string, data orm.QueryData) ![]orm.Primitive { + mut map_val := db.get_table_data_type_map(table)! + + // adapt v type to sql time + mut converted_data := []orm.Primitive{} + for i, field in data.fields { + match data.data[i].type_name() { + 'time.Time' { + if map_val[field] == 'datetime' { + converted_data << orm.Primitive((data.data[i] as time.Time).str()) + } else { + converted_data << data.data[i] + } + } + else { + converted_data << data.data[i] + } + } + } + return converted_data +} + +fn (db Connection) get_table_data_type_map(table string) !map[string]string { + data_type_querys := "SELECT COLUMN_NAME, DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '${table}'" + mut map_val := map[string]string{} + + results := db.query(data_type_querys)! + db.use_result() + + for row in results.rows() { + map_val[row.vals[0]] = row.vals[1] + } + + unsafe { results.free() } + return map_val +} diff --git a/vlib/db/mysql/result.v b/vlib/db/mysql/result.v new file mode 100644 index 0000000000..0969c75c20 --- /dev/null +++ b/vlib/db/mysql/result.v @@ -0,0 +1,153 @@ +module mysql + +pub struct Result { + 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() &&u8 { + return C.mysql_fetch_row(r.result) +} + +// n_rows - returns the number of rows from a result. +pub fn (r Result) n_rows() u64 { + return C.mysql_num_rows(r.result) +} + +// n_fields - returns the number of columns from a result. +pub fn (r Result) n_fields() int { + 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 rows := []Row{} + nr_cols := r.n_fields() + for rr := r.fetch_row(); rr; rr = r.fetch_row() { + 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 fields := []Field{} + nr_cols := r.n_fields() + 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() { + C.mysql_free_result(r.result) +} diff --git a/vlib/db/mysql/stmt.c.v b/vlib/db/mysql/stmt.c.v new file mode 100644 index 0000000000..4e6233e23b --- /dev/null +++ b/vlib/db/mysql/stmt.c.v @@ -0,0 +1,285 @@ +module mysql + +[typedef] +struct C.MYSQL_STMT { + mysql &C.MYSQL + stmt_id u32 +} + +[typedef] +struct C.MYSQL_BIND { +mut: + buffer_type int + buffer voidptr + buffer_length u32 + length &u32 +} + +const ( + mysql_type_decimal = C.MYSQL_TYPE_DECIMAL + mysql_type_tiny = C.MYSQL_TYPE_TINY + mysql_type_short = C.MYSQL_TYPE_SHORT + mysql_type_long = C.MYSQL_TYPE_LONG + mysql_type_float = C.MYSQL_TYPE_FLOAT + mysql_type_double = C.MYSQL_TYPE_DOUBLE + mysql_type_null = C.MYSQL_TYPE_NULL + mysql_type_timestamp = C.MYSQL_TYPE_TIMESTAMP + mysql_type_longlong = C.MYSQL_TYPE_LONGLONG + mysql_type_int24 = C.MYSQL_TYPE_INT24 + mysql_type_date = C.MYSQL_TYPE_DATE + mysql_type_time = C.MYSQL_TYPE_TIME + mysql_type_datetime = C.MYSQL_TYPE_DATETIME + mysql_type_year = C.MYSQL_TYPE_YEAR + mysql_type_varchar = C.MYSQL_TYPE_VARCHAR + mysql_type_bit = C.MYSQL_TYPE_BIT + mysql_type_timestamp22 = C.MYSQL_TYPE_TIMESTAMP + mysql_type_json = C.MYSQL_TYPE_JSON + mysql_type_newdecimal = C.MYSQL_TYPE_NEWDECIMAL + mysql_type_enum = C.MYSQL_TYPE_ENUM + mysql_type_set = C.MYSQL_TYPE_SET + mysql_type_tiny_blob = C.MYSQL_TYPE_TINY_BLOB + mysql_type_medium_blob = C.MYSQL_TYPE_MEDIUM_BLOB + mysql_type_long_blob = C.MYSQL_TYPE_LONG_BLOB + mysql_type_blob = C.MYSQL_TYPE_BLOB + mysql_type_var_string = C.MYSQL_TYPE_VAR_STRING + mysql_type_string = C.MYSQL_TYPE_STRING + mysql_type_geometry = C.MYSQL_TYPE_GEOMETRY + 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, &char, u32) int +fn C.mysql_stmt_bind_param(&C.MYSQL_STMT, &C.MYSQL_BIND) bool +fn C.mysql_stmt_execute(&C.MYSQL_STMT) int +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_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) int +fn C.mysql_stmt_next_result(&C.MYSQL_STMT) int +fn C.mysql_stmt_store_result(&C.MYSQL_STMT) int + +pub struct Stmt { + stmt &C.MYSQL_STMT = &C.MYSQL_STMT(0) + query string +mut: + binds []C.MYSQL_BIND + res []C.MYSQL_BIND +} + +// 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 Connection) 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() ! { + res := C.mysql_stmt_prepare(stmt.stmt, stmt.query.str, stmt.query.len) + if res != 0 && stmt.get_error_msg() != '' { + return stmt.error(res) + } +} + +// bind_params binds all the parameters in `stmt` +pub fn (stmt Stmt) bind_params() ! { + res := C.mysql_stmt_bind_param(stmt.stmt, unsafe { &C.MYSQL_BIND(stmt.binds.data) }) + if res && stmt.get_error_msg() != '' { + return stmt.error(1) + } +} + +// execute executes the given `stmt` and waits for the result +pub fn (stmt Stmt) execute() !int { + res := C.mysql_stmt_execute(stmt.stmt) + if res != 0 && stmt.get_error_msg() != '' { + return stmt.error(res) + } + return res +} + +// next retrieves the next available result from the execution of `stmt` +pub fn (stmt Stmt) next() !int { + res := C.mysql_stmt_next_result(stmt.stmt) + if res > 0 && stmt.get_error_msg() != '' { + return stmt.error(res) + } + return res +} + +// 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 { + res := C.mysql_stmt_fetch(stmt.stmt) + if res !in [0, 100] && stmt.get_error_msg() != '' { + return stmt.error(res) + } + return res +} + +// 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 unsafe { cstring_to_vstring(&char(C.mysql_stmt_error(stmt.stmt))) } +} + +// error returns a proper V error with a human readable description, given the error code returned by MySQL +pub fn (stmt Stmt) error(code int) IError { + msg := stmt.get_error_msg() + 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.mysql_type_tiny, b, 0) +} + +// bind_byte binds a single byte value to the statement `stmt` +pub fn (mut stmt Stmt) bind_byte(b &byte) { + stmt.bind(mysql.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.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.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.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.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.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.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.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.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.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.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.mysql_type_string, b.str, u32(b.len)) +} + +// 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 + } +} + +// bind_res will store one result in the statement `stmt` +pub fn (mut stmt Stmt) bind_res(fields &C.MYSQL_FIELD, dataptr []&u8, lens []u32, num_fields int) { + for i in 0 .. num_fields { + len := unsafe { FieldType(fields[i].@type).get_len() } + stmt.res << C.MYSQL_BIND{ + buffer_type: unsafe { fields[i].@type } + buffer: dataptr[i] + length: &lens[i] + buffer_length: len + } + } +} + +// 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() ! { + res := C.mysql_stmt_bind_result(stmt.stmt, unsafe { &C.MYSQL_BIND(stmt.res.data) }) + if res && 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() ! { + res := C.mysql_stmt_store_result(stmt.stmt) + if res != 0 && stmt.get_error_msg() != '' { + return stmt.error(res) + } +} diff --git a/vlib/db/mysql/utils.v b/vlib/db/mysql/utils.v new file mode 100644 index 0000000000..d35554c805 --- /dev/null +++ b/vlib/db/mysql/utils.v @@ -0,0 +1,26 @@ +module mysql + +// get_error_msg - returns error message from MySQL instance. +fn get_error_msg(conn &C.MYSQL) string { + return unsafe { C.mysql_error(conn).vstring() } +} + +// get_errno - returns error number from MySQL instance. +fn get_errno(conn &C.MYSQL) int { + return C.mysql_errno(conn) +} + +// resolve_nil_str - returns an empty string if passed value is a nil pointer. +fn resolve_nil_str(ptr &u8) string { + if isnil(ptr) { + return '' + } + return unsafe { ptr.vstring() } +} + +[inline] +fn mystring(b &u8) string { + unsafe { + return b.vstring() + } +} diff --git a/vlib/db/pg/README.md b/vlib/db/pg/README.md new file mode 100644 index 0000000000..551dadc775 --- /dev/null +++ b/vlib/db/pg/README.md @@ -0,0 +1,52 @@ +## 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-get 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 +``` + +### 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-get install libpq-dev` + +**Red Hat Linux (RHEL)**: `yum install postgresql-devel` + +**OpenSuse**: `zypper in postgresql-devel` + +**ArchLinux**: `pacman -S postgresql-libs` + +##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)*. diff --git a/vlib/db/pg/compatibility.h b/vlib/db/pg/compatibility.h new file mode 100644 index 0000000000..3d82f22111 --- /dev/null +++ b/vlib/db/pg/compatibility.h @@ -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 diff --git a/vlib/db/pg/oid.v b/vlib/db/pg/oid.v new file mode 100644 index 0000000000..2f8004d888 --- /dev/null +++ b/vlib/db/pg/oid.v @@ -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 +} diff --git a/vlib/db/pg/orm.v b/vlib/db/pg/orm.v new file mode 100644 index 0000000000..0cd4ce63bb --- /dev/null +++ b/vlib/db/pg/orm.v @@ -0,0 +1,298 @@ +module pg + +import orm +import time +import net.conv + +// sql expr + +// @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 { + query := orm.orm_select_gen(config, '"', true, '$', 1, where) + + res := pg_stmt_worker(db, query, where, data)! + + mut ret := [][]orm.Primitive{} + + if config.is_count { + } + + for row in res { + mut row_data := []orm.Primitive{} + for i, val in row.vals { + field := str_to_primitive(val, config.types[i])! + row_data << field + } + ret << row_data + } + + return ret +} + +// sql stmt + +// insert is used internally by V's ORM for processing `INSERT ` queries +pub fn (db DB) insert(table string, data orm.QueryData) ! { + query, converted_data := orm.orm_stmt_gen(table, '"', .insert, true, '$', 1, data, + orm.QueryData{}) + pg_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 string, data orm.QueryData, where orm.QueryData) ! { + query, _ := orm.orm_stmt_gen(table, '"', .update, true, '$', 1, data, where) + pg_stmt_worker(db, query, data, where)! +} + +// delete is used internally by V's ORM for processing `DELETE ` queries +pub fn (db DB) delete(table string, where orm.QueryData) ! { + query, _ := orm.orm_stmt_gen(table, '"', .delete, true, '$', 1, orm.QueryData{}, where) + pg_stmt_worker(db, query, orm.QueryData{}, where)! +} + +// last_id is used internally by V's ORM for post-processing `INSERT ` queries +pub fn (db DB) last_id() orm.Primitive { + query := 'SELECT LASTVAL();' + id := db.q_int(query) or { 0 } + return orm.Primitive(id) +} + +// 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 string, fields []orm.TableField) ! { + query := orm.orm_table_gen(table, '"', true, 0, fields, pg_type_from_v, false) or { return err } + pg_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 string) ! { + query := 'DROP TABLE "${table}";' + pg_stmt_worker(db, query, orm.QueryData{}, orm.QueryData{})! +} + +// utils + +fn pg_stmt_worker(db DB, query string, data orm.QueryData, where orm.QueryData) ![]Row { + mut param_types := []u32{} + mut param_vals := []&char{} + mut param_lens := []int{} + mut param_formats := []int{} + + pg_stmt_binder(mut param_types, mut param_vals, mut param_lens, mut param_formats, + data) + pg_stmt_binder(mut param_types, mut param_vals, mut param_lens, mut param_formats, + where) + + res := C.PQexecParams(db.conn, &char(query.str), param_vals.len, param_types.data, + param_vals.data, param_lens.data, param_formats.data, 0) // here, the last 0 means require text results, 1 - binary results + return db.handle_error_or_result(res, 'orm_stmt_worker') +} + +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(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.htn16(data) + vals << &char(&num) + lens << int(sizeof(u16)) + formats << 1 + } + u32 { + types << u32(Oid.t_int4) + num := conv.htn32(data) + vals << &char(&num) + lens << int(sizeof(u32)) + formats << 1 + } + u64 { + types << u32(Oid.t_int8) + num := conv.htn64(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.htn16(u16(data)) + vals << &char(&num) + lens << int(sizeof(i16)) + formats << 1 + } + int { + types << u32(Oid.t_int4) + num := conv.htn32(u32(data)) + vals << &char(&num) + lens << int(sizeof(int)) + formats << 1 + } + i64 { + types << u32(Oid.t_int8) + num := conv.htn64(u64(data)) + vals << &char(&num) + lens << int(sizeof(i64)) + formats << 1 + } + f32 { + types << u32(Oid.t_float4) + vals << &char(&data) + lens << int(sizeof(f32)) + formats << 1 + } + f64 { + types << u32(Oid.t_float8) + vals << &char(&data) + 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) + } + } +} + +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.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 str_to_primitive(str string, typ int) !orm.Primitive { + 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)) + } + else {} + } + return error('Unknown field type ${typ}') +} diff --git a/vlib/db/pg/pg.v b/vlib/db/pg/pg.v new file mode 100644 index 0000000000..d3b5a917cc --- /dev/null +++ b/vlib/db/pg/pg.v @@ -0,0 +1,348 @@ +module pg + +import io + +$if $pkgconfig('libpq') { + #pkgconfig --cflags --libs libpq +} $else { + #flag -lpq + #flag linux -I/usr/include/postgresql + + #flag darwin -I/opt/local/include/postgresql11 + #flag darwin -L/opt/local/lib/postgresql11 + + #flag darwin -I/usr/local/opt/libpq/include + #flag darwin -L/usr/local/opt/libpq/lib + + #flag darwin -I/opt/homebrew/include + #flag darwin -L/opt/homebrew/lib + + #flag darwin -I/opt/homebrew/opt/libpq/include + #flag darwin -L/opt/homebrew/opt/libpq/lib + + #flag windows -I @VEXEROOT/thirdparty/pg/include + #flag windows -L @VEXEROOT/thirdparty/pg/win64 +} + +// PostgreSQL Source Code +// https://doxygen.postgresql.org/libpq-fe_8h.html +#include + +// for PG_VERSION_NUM, which is defined everywhere at least since PG 9.5 +#include + +// for orm +#include + +#include "@VMODROOT/vlib/db/pg/compatibility.h" + +pub struct DB { +mut: + conn voidptr = unsafe { nil } +} + +pub struct Row { +pub mut: + vals []string +} + +pub struct Config { +pub: + host string = 'localhost' + port int = 5432 + user string + password string + dbname string +} + +// + +struct C.pg_result {} + +struct C.pg_conn {} + +[typedef] +pub struct C.PGresult {} + +[typedef] +pub struct C.PGconn {} + +pub enum ConnStatusType { + ok = C.CONNECTION_OK + bad = C.CONNECTION_BAD + // Non-blocking mode only below here + // The existence of these should never be relied upon - they should only be used for user feedback or similar purposes. + started = C.CONNECTION_STARTED // Waiting for connection to be made. + made = C.CONNECTION_MADE // Connection OK; waiting to send. + awaiting_response = C.CONNECTION_AWAITING_RESPONSE // Waiting for a response from the postmaster. + auth_ok = C.CONNECTION_AUTH_OK // Received authentication; waiting for backend startup. + setenv = C.CONNECTION_SETENV // Negotiating environment. + ssl_startup = C.CONNECTION_SSL_STARTUP // Negotiating SSL. + needed = C.CONNECTION_NEEDED // Internal state: connect() needed . Available in PG 8 + check_writable = C.CONNECTION_CHECK_WRITABLE // Check if we could make a writable connection. Available since PG 10 + consume = C.CONNECTION_CONSUME // Wait for any pending message and consume them. Available since PG 10 + gss_startup = C.CONNECTION_GSS_STARTUP // Negotiating GSSAPI; available since PG 12 +} + +[typedef] +pub enum ExecStatusType { + empty_query = C.PGRES_EMPTY_QUERY // empty query string was executed + command_ok = C.PGRES_COMMAND_OK // a query command that doesn't return anything was executed properly by the backend + tuples_ok = C.PGRES_TUPLES_OK // a query command that returns tuples was executed properly by the backend, PGresult contains the result tuples + copy_out = C.PGRES_COPY_OUT // Copy Out data transfer in progress + copy_in = C.PGRES_COPY_IN // Copy In data transfer in progress + bad_response = C.PGRES_BAD_RESPONSE // an unexpected response was recv'd from the backend + nonfatal_error = C.PGRES_NONFATAL_ERROR // notice or warning message + fatal_error = C.PGRES_FATAL_ERROR // query failed + copy_both = C.PGRES_COPY_BOTH // Copy In/Out data transfer in progress + single_tuple = C.PGRES_SINGLE_TUPLE // single tuple from larger resultset +} + +// + +fn C.PQconnectdb(const_conninfo &char) &C.PGconn + +fn C.PQstatus(const_conn &C.PGconn) int + +fn C.PQerrorMessage(const_conn &C.PGconn) &char + +fn C.PQexec(res &C.PGconn, const_query &char) &C.PGresult + +// + +fn C.PQgetvalue(const_res &C.PGresult, int, int) &char + +fn C.PQresultStatus(const_res &C.PGresult) int + +fn C.PQntuples(const_res &C.PGresult) int + +fn C.PQnfields(const_res &C.PGresult) int + +// Params: +// const Oid *paramTypes +// const char *const *paramValues +// const int *paramLengths +// const int *paramFormats +fn C.PQexecParams(conn &C.PGconn, const_command &char, nParams int, const_paramTypes &int, const_paramValues &char, const_paramLengths &int, const_paramFormats &int, resultFormat int) &C.PGresult + +fn C.PQputCopyData(conn &C.PGconn, const_buffer &char, nbytes int) int + +fn C.PQputCopyEnd(conn &C.PGconn, const_errmsg &char) int + +fn C.PQgetCopyData(conn &C.PGconn, buffer &&char, async int) int + +// cleanup + +fn C.PQclear(res &C.PGresult) + +fn C.PQfreemem(ptr voidptr) + +fn C.PQfinish(conn &C.PGconn) + +// connect makes a new connection to the database server using +// the parameters from the `Config` structure, returning +// a connection error when something goes wrong +pub fn connect(config Config) !DB { + conninfo := 'host=${config.host} port=${config.port} user=${config.user} dbname=${config.dbname} password=${config.password}' + conn := C.PQconnectdb(&char(conninfo.str)) + if conn == 0 { + return error('libpq memory allocation error') + } + status := unsafe { ConnStatusType(C.PQstatus(conn)) } + if status != .ok { + // We force the construction of a new string as the + // error message will be freed by the next `PQfinish` + // call + c_error_msg := unsafe { C.PQerrorMessage(conn).vstring() } + error_msg := '${c_error_msg}' + C.PQfinish(conn) + return error('Connection to a PG database failed: ${error_msg}') + } + return DB{ + conn: conn + } +} + +fn res_to_rows(res voidptr) []Row { + nr_rows := C.PQntuples(res) + nr_cols := C.PQnfields(res) + + mut rows := []Row{} + for i in 0 .. nr_rows { + mut row := Row{} + for j in 0 .. nr_cols { + val := C.PQgetvalue(res, i, j) + sval := unsafe { val.vstring() } + row.vals << sval + } + rows << row + } + + C.PQclear(res) + return rows +} + +// close frees the underlying resource allocated by the database connection +pub fn (db DB) close() { + C.PQfinish(db.conn) +} + +// q_int submit a command to the database server and +// returns an the first field in the first tuple +// converted to an int. If no row is found or on +// command failure, an error is returned +pub fn (db DB) q_int(query string) !int { + rows := db.exec(query)! + if rows.len == 0 { + return error('q_int "${query}" not found') + } + row := rows[0] + if row.vals.len == 0 { + return 0 + } + val := row.vals[0] + return val.int() +} + +// q_string submit a command to the database server and +// returns an the first field in the first tuple +// as a string. If no row is found or on +// command failure, an error is returned +pub fn (db DB) q_string(query string) !string { + rows := db.exec(query)! + if rows.len == 0 { + return error('q_string "${query}" not found') + } + row := rows[0] + if row.vals.len == 0 { + return '' + } + val := row.vals[0] + return val +} + +// q_strings submit a command to the database server and +// returns the resulting row set. Alias of `exec` +pub fn (db DB) q_strings(query string) ![]Row { + return db.exec(query) +} + +// exec submits a command to the database server and wait for the result, returning an error on failure and a row set on success +pub fn (db DB) exec(query string) ![]Row { + res := C.PQexec(db.conn, &char(query.str)) + return db.handle_error_or_result(res, 'exec') +} + +fn rows_first_or_empty(rows []Row) !Row { + if rows.len == 0 { + return error('no row') + } + return rows[0] +} + +// exec_one executes a query and returns its first row as a result, or an error on failure +pub fn (db DB) exec_one(query string) !Row { + res := C.PQexec(db.conn, &char(query.str)) + e := unsafe { C.PQerrorMessage(db.conn).vstring() } + if e != '' { + return error('pg exec error: "${e}"') + } + row := rows_first_or_empty(res_to_rows(res))! + return row +} + +// exec_param_many executes a query with the provided parameters +pub fn (db DB) exec_param_many(query string, params []string) ![]Row { + unsafe { + mut param_vals := []&char{len: params.len} + for i in 0 .. params.len { + param_vals[i] = &char(params[i].str) + } + + res := C.PQexecParams(db.conn, &char(query.str), params.len, 0, param_vals.data, + 0, 0, 0) + return db.handle_error_or_result(res, 'exec_param_many') + } +} + +// exec_param2 executes a query with 1 parameter, 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 2 parameters, and returns either an error on failure, or the full result set on success +pub fn (db DB) exec_param2(query string, param string, param2 string) ![]Row { + return db.exec_param_many(query, [param, param2]) +} + +fn (db DB) handle_error_or_result(res voidptr, elabel string) ![]Row { + e := unsafe { C.PQerrorMessage(db.conn).vstring() } + if e != '' { + C.PQclear(res) + return error('pg ${elabel} error:\n${e}') + } + return res_to_rows(res) +} + +// copy_expert executes COPY command +// https://www.postgresql.org/docs/9.5/libpq-copy.html +pub fn (db DB) copy_expert(query string, mut file io.ReaderWriter) !int { + mut res := C.PQexec(db.conn, &char(query.str)) + status := unsafe { ExecStatusType(C.PQresultStatus(res)) } + defer { + C.PQclear(res) + } + + e := unsafe { C.PQerrorMessage(db.conn).vstring() } + if e != '' { + return error('pg copy error:\n${e}') + } + + if status == .copy_in { + mut buf := []u8{len: 4 * 1024} + for { + n := file.read(mut buf) or { + msg := 'pg copy error: Failed to read from input' + C.PQputCopyEnd(db.conn, &char(msg.str)) + return err + } + if n <= 0 { + break + } + + code := C.PQputCopyData(db.conn, buf.data, n) + if code == -1 { + return error('pg copy error: Failed to send data, code=${code}') + } + } + + code := C.PQputCopyEnd(db.conn, &char(0)) + + if code != 1 { + return error('pg copy error: Failed to finish copy command, code: ${code}') + } + } else if status == .copy_out { + for { + address := &char(0) + n_bytes := C.PQgetCopyData(db.conn, &address, 0) + if n_bytes > 0 { + mut local_buf := []u8{len: n_bytes} + unsafe { C.memcpy(&u8(local_buf.data), address, n_bytes) } + file.write(local_buf) or { + C.PQfreemem(address) + return err + } + } else if n_bytes == -1 { + break + } else if n_bytes == -2 { + // consult PQerrorMessage for the reason + return error('pg copy error: read error') + } + if address != 0 { + C.PQfreemem(address) + } + } + } + + return 0 +} diff --git a/vlib/pg/pg_orm_test.v b/vlib/db/pg/pg_orm_test.v similarity index 99% rename from vlib/pg/pg_orm_test.v rename to vlib/db/pg/pg_orm_test.v index cf2e628e79..f6f2162fbd 100644 --- a/vlib/pg/pg_orm_test.v +++ b/vlib/db/pg/pg_orm_test.v @@ -1,7 +1,7 @@ module main import orm -import pg +import db.pg import time struct TestCustomSqlType { diff --git a/vlib/db/sqlite/README.md b/vlib/db/sqlite/README.md new file mode 100644 index 0000000000..656b394703 --- /dev/null +++ b/vlib/db/sqlite/README.md @@ -0,0 +1,39 @@ +## 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 + +Before you can use this module, you must first have the SQLite development +library installed on your system. + +**Fedora 31**: + +`sudo dnf -y install sqlite-devel` + + +**Ubuntu 20.04**: + +`sudo apt install -y libsqlite3-dev` + + +**Windows**: +- Download the source zip from [SQLite Downloads](https://sqlite.org/download.html) +- Create a new `sqlite` subfolder inside `v/thirdparty` +- Extract the zip into that folder + +# 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) +``` diff --git a/vlib/db/sqlite/orm.v b/vlib/db/sqlite/orm.v new file mode 100644 index 0000000000..dd9a1d1ced --- /dev/null +++ b/vlib/db/sqlite/orm.v @@ -0,0 +1,184 @@ +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 { + // 1. Create query and bind necessary data + query := orm.orm_select_gen(config, '`', true, '?', 1, where) + $if trace_sqlite ? { + eprintln('> @select query: "${query}"') + } + stmt := db.new_init_stmt(query)! + defer { + stmt.finalize() + } + mut c := 1 + sqlite_stmt_binder(stmt, where, query, mut c)! + sqlite_stmt_binder(stmt, data, query, mut c)! + + mut ret := [][]orm.Primitive{} + + if config.is_count { + // 2. Get count of returned values & add it to ret array + step := stmt.step() + if step !in [sqlite_row, sqlite_ok, sqlite_done] { + return db.error_message(step, query) + } + count := stmt.sqlite_select_column(0, 8)! + ret << [count] + return ret + } + 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 string, data orm.QueryData) ! { + query, converted_data := orm.orm_stmt_gen(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 string, data orm.QueryData, where orm.QueryData) ! { + query, _ := orm.orm_stmt_gen(table, '`', .update, true, '?', 1, data, where) + sqlite_stmt_worker(db, query, data, where)! +} + +// delete is used internally by V's ORM for processing `DELETE ` queries +pub fn (db DB) delete(table string, where orm.QueryData) ! { + query, _ := orm.orm_stmt_gen(table, '`', .delete, true, '?', 1, orm.QueryData{}, where) + sqlite_stmt_worker(db, query, orm.QueryData{}, where)! +} + +// last_id is used internally by V's ORM for post-processing `INSERT ` queries +pub fn (db DB) last_id() orm.Primitive { + query := 'SELECT last_insert_rowid();' + id := db.q_int(query) + return orm.Primitive(id) +} + +// 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 string, fields []orm.TableField) ! { + query := orm.orm_table_gen(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 string) ! { + query := 'DROP TABLE `${table}`;' + sqlite_stmt_worker(db, query, orm.QueryData{}, orm.QueryData{})! +} + +// 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, c, data) + + if err != 0 { + return stmt.db.error_message(err, query) + } + c++ + } +} + +// Universal bind function +fn bind(stmt Stmt, 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, unsafe { *(&f64(&data)) }) + } + string { + err = stmt.bind_text(c, data) + } + time.Time { + err = stmt.bind_int(c, int(data.unix)) + } + orm.InfixType { + err = bind(stmt, c, data.right) + } + } + 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 { + mut primitive := orm.Primitive(0) + + if typ in orm.nums || typ == -1 { + primitive = stmt.get_int(idx) + } else if typ in orm.num64 { + primitive = stmt.get_i64(idx) + } else if typ in orm.float { + primitive = stmt.get_f64(idx) + } else if typ == orm.type_string { + primitive = stmt.get_text(idx).clone() + } else if typ == orm.time { + d := stmt.get_int(idx) + primitive = time.unix(d) + } else { + return error('Unknown type ${typ}') + } + + return primitive +} + +// Convert type int to sql type string +fn sqlite_type_from_v(typ int) !string { + return if typ in orm.nums || typ < 0 || typ in orm.num64 || typ == orm.time { + 'INTEGER' + } else if typ in orm.float { + 'REAL' + } else if typ == orm.type_string { + 'TEXT' + } else { + error('Unknown type ${typ}') + } +} diff --git a/vlib/db/sqlite/result_code.v b/vlib/db/sqlite/result_code.v new file mode 100644 index 0000000000..f793d283ed --- /dev/null +++ b/vlib/db/sqlite/result_code.v @@ -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() } +} diff --git a/vlib/db/sqlite/sqlite.v b/vlib/db/sqlite/sqlite.v new file mode 100644 index 0000000000..7a95cc9ff5 --- /dev/null +++ b/vlib/db/sqlite/sqlite.v @@ -0,0 +1,340 @@ +module sqlite + +$if freebsd || openbsd { + #flag -I/usr/local/include + #flag -L/usr/local/lib +} +$if windows { + #flag windows -I@VEXEROOT/thirdparty/sqlite + #flag windows -L@VEXEROOT/thirdparty/sqlite + #flag windows @VEXEROOT/thirdparty/sqlite/sqlite3.o +} $else { + #flag -lsqlite3 +} + +#include "sqlite3.h" + +// https://www.sqlite.org/rescode.html +pub const ( + sqlite_ok = 0 + sqlite_error = 1 + sqlite_row = 100 + sqlite_done = 101 + sqlite_cantopen = 14 + sqlite_ioerr_read = 266 + sqlite_ioerr_short_read = 522 + sqlite_ioerr_write = 778 + sqlite_ioerr_fsync = 1034 + sqlite_ioerr_fstat = 1802 + sqlite_ioerr_delete = 2570 + + sqlite_open_main_db = 0x00000100 + sqlite_open_temp_db = 0x00000200 + sqlite_open_transient_db = 0x00000400 + sqlite_open_main_journal = 0x00000800 + sqlite_open_temp_journal = 0x00001000 + sqlite_open_subjournal = 0x00002000 + sqlite_open_super_journal = 0x00004000 + sqlite_open_wal = 0x00080000 +) + +pub enum SyncMode { + off + normal + full +} + +pub enum JournalMode { + off + delete + truncate + persist + memory +} + +struct C.sqlite3 { +} + +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 +} + +// +fn C.sqlite3_open(&char, &&C.sqlite3) int + +fn C.sqlite3_close(&C.sqlite3) int + +fn C.sqlite3_busy_timeout(db &C.sqlite3, ms int) int + +fn C.sqlite3_last_insert_rowid(&C.sqlite3) i64 + +// +fn C.sqlite3_prepare_v2(&C.sqlite3, &char, int, &&C.sqlite3_stmt, &&char) int + +fn C.sqlite3_step(&C.sqlite3_stmt) int + +fn C.sqlite3_finalize(&C.sqlite3_stmt) int + +// +fn C.sqlite3_column_name(&C.sqlite3_stmt, int) &char + +fn C.sqlite3_column_text(&C.sqlite3_stmt, int) &u8 + +fn C.sqlite3_column_int(&C.sqlite3_stmt, int) int + +fn C.sqlite3_column_int64(&C.sqlite3_stmt, int) i64 + +fn C.sqlite3_column_double(&C.sqlite3_stmt, int) f64 + +fn C.sqlite3_column_count(&C.sqlite3_stmt) int + +// +fn C.sqlite3_errstr(int) &char + +fn C.sqlite3_errmsg(&C.sqlite3) &char + +fn C.sqlite3_free(voidptr) + +fn C.sqlite3_changes(&C.sqlite3) int + +// connect Opens the connection with a database. +pub fn connect(path string) !DB { + db := &C.sqlite3(0) + code := C.sqlite3_open(&char(path.str), &db) + if code != 0 { + return &SQLError{ + msg: unsafe { cstring_to_vstring(&char(C.sqlite3_errstr(code))) } + 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() !bool { + code := C.sqlite3_close(db.conn) + if code == 0 { + db.is_open = false + } else { + return &SQLError{ + msg: unsafe { cstring_to_vstring(&char(C.sqlite3_errstr(code))) } + code: code + } + } + return true // successfully closed +} + +// 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` +pub fn (db &DB) q_int(query string) int { + stmt := &C.sqlite3_stmt(0) + defer { + C.sqlite3_finalize(stmt) + } + C.sqlite3_prepare_v2(db.conn, &char(query.str), query.len, &stmt, 0) + C.sqlite3_step(stmt) + + 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` +pub fn (db &DB) q_string(query string) string { + stmt := &C.sqlite3_stmt(0) + defer { + C.sqlite3_finalize(stmt) + } + C.sqlite3_prepare_v2(db.conn, &char(query.str), query.len, &stmt, 0) + C.sqlite3_step(stmt) + + val := unsafe { &u8(C.sqlite3_column_text(stmt, 0)) } + return if val != &u8(0) { unsafe { tos_clone(val) } } else { '' } +} + +// exec executes the query on the given `db`, and returns an array of all the results, alongside any result code. +// Result codes: https://www.sqlite.org/rescode.html +[manualfree] +pub fn (db &DB) exec(query string) ([]Row, int) { + stmt := &C.sqlite3_stmt(0) + defer { + C.sqlite3_finalize(stmt) + } + C.sqlite3_prepare_v2(db.conn, &char(query.str), query.len, &stmt, 0) + nr_cols := C.sqlite3_column_count(stmt) + mut res := 0 + mut rows := []Row{} + for { + res = C.sqlite3_step(stmt) + // Result Code SQLITE_ROW; Another row is available + if res != 100 { + // C.puts(C.sqlite3_errstr(res)) + break + } + mut row := Row{} + for i in 0 .. nr_cols { + val := unsafe { &u8(C.sqlite3_column_text(stmt, i)) } + if val == &u8(0) { + row.vals << '' + } else { + row.vals << unsafe { tos_clone(val) } + } + } + rows << row + } + return rows, res +} + +// 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, code := db.exec(query) + defer { + unsafe { rows.free() } + } + if rows.len == 0 { + return &SQLError{ + msg: 'No rows' + code: code + } + } else if code != 101 { + return &SQLError{ + msg: unsafe { cstring_to_vstring(&char(C.sqlite3_errstr(code))) } + code: code + } + } + 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 { + stmt := &C.sqlite3_stmt(0) + C.sqlite3_prepare_v2(db.conn, &char(query.str), query.len, &stmt, 0) + code := C.sqlite3_step(stmt) + C.sqlite3_finalize(stmt) + return code +} + +// TODO pub fn (db &DB) exec_param(query string, param string) []Row { + +// 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 (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. +// .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. +// .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;') + } +} diff --git a/vlib/sqlite/sqlite_orm_test.v b/vlib/db/sqlite/sqlite_orm_test.v similarity index 99% rename from vlib/sqlite/sqlite_orm_test.v rename to vlib/db/sqlite/sqlite_orm_test.v index a38a45220f..82527668c5 100644 --- a/vlib/sqlite/sqlite_orm_test.v +++ b/vlib/db/sqlite/sqlite_orm_test.v @@ -1,5 +1,5 @@ import orm -import sqlite +import db.sqlite import time struct TestCustomSqlType { diff --git a/vlib/sqlite/sqlite_test.v b/vlib/db/sqlite/sqlite_test.v similarity index 99% rename from vlib/sqlite/sqlite_test.v rename to vlib/db/sqlite/sqlite_test.v index 2e93b8ac5c..4ab454361f 100644 --- a/vlib/sqlite/sqlite_test.v +++ b/vlib/db/sqlite/sqlite_test.v @@ -1,4 +1,4 @@ -import sqlite +import db.sqlite type Connection = sqlite.DB diff --git a/vlib/sqlite/sqlite_vfs_lowlevel_test.v b/vlib/db/sqlite/sqlite_vfs_lowlevel_test.v similarity index 99% rename from vlib/sqlite/sqlite_vfs_lowlevel_test.v rename to vlib/db/sqlite/sqlite_vfs_lowlevel_test.v index 85e1807e61..427503e6d6 100644 --- a/vlib/sqlite/sqlite_vfs_lowlevel_test.v +++ b/vlib/db/sqlite/sqlite_vfs_lowlevel_test.v @@ -1,4 +1,4 @@ -import sqlite +import db.sqlite import rand const ( diff --git a/vlib/db/sqlite/stmt.v b/vlib/db/sqlite/stmt.v new file mode 100644 index 0000000000..74ebc3be82 --- /dev/null +++ b/vlib/db/sqlite/stmt.v @@ -0,0 +1,78 @@ +module sqlite + +fn C.sqlite3_bind_double(&C.sqlite3_stmt, int, f64) int +fn C.sqlite3_bind_int(&C.sqlite3_stmt, int, int) int +fn C.sqlite3_bind_int64(&C.sqlite3_stmt, int, i64) int +fn C.sqlite3_bind_text(&C.sqlite3_stmt, int, &char, int, voidptr) int + +// Only for V ORM +fn (db &DB) init_stmt(query string) (&C.sqlite3_stmt, int) { + // println('init_stmt("$query")') + stmt := &C.sqlite3_stmt(0) + 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_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 { + return C.sqlite3_column_int(stmt.stmt, idx) +} + +fn (stmt &Stmt) get_i64(idx int) i64 { + return C.sqlite3_column_int64(stmt.stmt, idx) +} + +fn (stmt &Stmt) get_f64(idx int) f64 { + return C.sqlite3_column_double(stmt.stmt, idx) +} + +fn (stmt &Stmt) get_text(idx int) string { + b := &char(C.sqlite3_column_text(stmt.stmt, idx)) + + if b == &char(0) { + return '' + } + return unsafe { b.vstring() } +} + +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) +} diff --git a/vlib/db/sqlite/vfs_lowlevel.v b/vlib/db/sqlite/vfs_lowlevel.v new file mode 100644 index 0000000000..db8abfe486 --- /dev/null +++ b/vlib/db/sqlite/vfs_lowlevel.v @@ -0,0 +1,171 @@ +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 + +// https://www.sqlite.org/c3ref/file.html +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] +struct C.sqlite3_io_methods { +mut: + // version 1 and later fields + iVersion int + + xClose fn (&Sqlite3_file) int + xRead fn (&Sqlite3_file, voidptr, int, i64) int + xWrite fn (&Sqlite3_file, voidptr, int, i64) int + xTruncate fn (&Sqlite3_file, i64) int + xSync fn (&Sqlite3_file, int) int + xFileSize Sig1 + xLock fn (&Sqlite3_file, int) int + xUnlock fn (&Sqlite3_file, int) int + xCheckReservedLock Sig2 + xFileControl fn (&Sqlite3_file, int, voidptr) int + xSectorSize fn (&Sqlite3_file) int + xDeviceCharacteristics fn (&Sqlite3_file) int + // version 2 and later fields + xShmMap fn (&Sqlite3_file, int, int, int, &voidptr) int + xShmLock fn (&Sqlite3_file, int, int, int) int + xShmBarrier fn (&Sqlite3_file) + xShmUnmap fn (&Sqlite3_file, int) int + // version 3 and later fields + xFetch fn (&Sqlite3_file, i64, int, &voidptr) int + xUnfetch fn (&Sqlite3_file, i64, voidptr) int +} + +pub type Sqlite3_io_methods = C.sqlite3_io_methods + +// https://www.sqlite.org/c3ref/vfs.html +type Fn_sqlite3_syscall_ptr = fn () + +pub type Sqlite3_vfs = C.sqlite3_vfs + +[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 fn (&Sqlite3_vfs, &char, &Sqlite3_file, int, &int) int + xDelete fn (&Sqlite3_vfs, &char, int) int + + xAccess fn (&Sqlite3_vfs, &char, int, &int) int + xFullPathname fn (&Sqlite3_vfs, &char, int, &char) int + xDlOpen fn (&Sqlite3_vfs, &char) voidptr + xDlError fn (&Sqlite3_vfs, int, &char) + xDlSym fn (&Sqlite3_vfs, voidptr, &char) voidptr // to fn accepting void and returning + xDlClose fn (&Sqlite3_vfs, voidptr) + xRandomness fn (&Sqlite3_vfs, int, &char) int + xSleep fn (&Sqlite3_vfs, int) int + xCurrentTime fn (&Sqlite3_vfs, &f64) int + xGetLastError fn (&Sqlite3_vfs, int, &char) int + // version two and later only fields + xCurrentTimeInt64 fn (&Sqlite3_vfs, &i64) int + // version three and later only fields + xSetSystemCall fn (&Sqlite3_vfs, &char, Fn_sqlite3_syscall_ptr) int + xGetSystemCall fn (&Sqlite3_vfs, &char) Fn_sqlite3_syscall_ptr + xNextSystemCall fn (&Sqlite3_vfs, &char) &char +} + +// 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, int) int +fn C.sqlite3_vfs_unregister(&C.sqlite3_vfs) int + +// 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) + + unsafe { + if res == nil { + return none + } else { + return res + } + } +} + +// get_default_vfs Asks sqlite for default VFS instance +pub fn get_default_vfs() ?&Sqlite3_vfs { + unsafe { + res := C.sqlite3_vfs_find(nil) + if res == nil { + return none + } else { + return res + } + } +} + +// 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) + + return if sqlite_ok == res { none } else { 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) + + return if sqlite_ok == res { none } else { error('sqlite3_vfs_unregister returned ${res}') } +} + +// https://www.sqlite.org/c3ref/open.html +fn C.sqlite3_open_v2(&char, &&C.sqlite3, int, &char) int + +// 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. +pub fn connect_full(path string, mode_flags []OpenModeFlag, vfs_name string) !DB { + db := &C.sqlite3(0) + + mut flags := 0 + + for flag in mode_flags { + flags = flags | int(flag) + } + + code := C.sqlite3_open_v2(&char(path.str), &db, flags, vfs_name.str) + if code != 0 { + return &SQLError{ + msg: unsafe { cstring_to_vstring(&char(C.sqlite3_errstr(code))) } + code: code + } + } + return DB{ + conn: db + is_open: true + } +} diff --git a/vlib/mssql/README.md b/vlib/mssql/README.md index 80f8181766..41baf2fa41 100644 --- a/vlib/mssql/README.md +++ b/vlib/mssql/README.md @@ -1,69 +1,4 @@ # SQL Server ODBC -* This is a V wrapper of SQL Server ODBC C/C++ library - -## Dependencies -* ODBC C/C++ library - * Linux Install: - * Details: https://docs.microsoft.com/en-us/sql/connect/odbc/linux-mac/installing-the-microsoft-odbc-driver-for-sql-server - * `msodbcsql17` and `unixodbc-dev` packages are needed - * Windows Install: - * `odbc` lib is included in windows sdk for most of distributions, - so there is no need to install it separately - * Details: https://docs.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 Mac -* Support 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: '' - pwd: '' - } - - 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 - } -} -``` +The `mssql` module has been moved to `db.mssql`. +Update your code to do: `import db.mssql` instead. diff --git a/vlib/mssql/z_deprecated.v b/vlib/mssql/z_deprecated.v new file mode 100644 index 0000000000..c09ca15705 --- /dev/null +++ b/vlib/mssql/z_deprecated.v @@ -0,0 +1,3 @@ +[deprecated: 'import db.mssql instead'] +[deprecated_after: '2023-02-01'] +module mssql diff --git a/vlib/mysql/README.md b/vlib/mysql/README.md index dec029fb84..4c1d359dc0 100644 --- a/vlib/mysql/README.md +++ b/vlib/mysql/README.md @@ -1,39 +1,4 @@ -For Linux, you need to install `MySQL development` package and `pkg-config`. +## Description: -For Windows, install [the installer](https://dev.mysql.com/downloads/installer/) , -then copy the `include` and `lib` folders to `\thirdparty\mysql`. - -Note: if you encounter weird errors (your program just exits right away, without -printing any messages, even though you have `println('hi')` statements in your -`fn main()`), when trying to run a program that does `import mysql` on windows, you -may need to copy the .dll file: `thirdparty/mysql/lib/libmysql.dll` , into the folder -of the executable too (it should be right next to the .exe file). -This is a temporary workaround, until we have a more permanent solution, or at least -more user friendly errors for that situation. - -## Basic Usage - -```v oksyntax -import mysql - -// Create connection -mut connection := mysql.Connection{ - username: 'root' - dbname: 'mysql' -} -// Connect to server -connection.connect()? -// Change the default database -connection.select_db('db_users')? -// Do a query -get_users_query_result := connection.query('SELECT * FROM users')? -// Get the result as maps -for user in get_users_query_result.maps() { - // Access the name of user - println(user['name']) -} -// Free the query result -get_users_query_result.free() -// Close the connection if needed -connection.close() -``` +The `mysql` module has been moved to `db.mysql`. +Update your code to do: `import db.mysql` instead. diff --git a/vlib/mysql/z_deprecated.v b/vlib/mysql/z_deprecated.v new file mode 100644 index 0000000000..888a601317 --- /dev/null +++ b/vlib/mysql/z_deprecated.v @@ -0,0 +1,3 @@ +[deprecated: 'import db.mysql instead'] +[deprecated_after: '2023-02-01'] +module mysql diff --git a/vlib/orm/README.md b/vlib/orm/README.md index 7ad3fc53cb..4f40e89aa5 100644 --- a/vlib/orm/README.md +++ b/vlib/orm/README.md @@ -112,7 +112,7 @@ result := sql db { ### Example ```v ignore -import pg +import db.pg struct Member { id string [default: 'gen_random_uuid()'; primary; sql_type: 'uuid'] diff --git a/vlib/orm/orm_sql_or_blocks_test.v b/vlib/orm/orm_sql_or_blocks_test.v index eed6baa43a..ac56f923da 100644 --- a/vlib/orm/orm_sql_or_blocks_test.v +++ b/vlib/orm/orm_sql_or_blocks_test.v @@ -1,5 +1,5 @@ import os -import sqlite +import db.sqlite struct User { id i64 [primary; sql: serial] diff --git a/vlib/orm/orm_test.v b/vlib/orm/orm_test.v index ce5c8b2f76..63fa6590c9 100644 --- a/vlib/orm/orm_test.v +++ b/vlib/orm/orm_test.v @@ -1,9 +1,9 @@ // import os // import term -// import mysql -// import pg +// import db.mysql +// import db.pg import time -import sqlite +import db.sqlite struct Module { id int [primary; sql: serial] diff --git a/vlib/pg/README.md b/vlib/pg/README.md index 551dadc775..91d35d535d 100644 --- a/vlib/pg/README.md +++ b/vlib/pg/README.md @@ -1,52 +1,4 @@ ## 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-get 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 -``` - -### 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-get install libpq-dev` - -**Red Hat Linux (RHEL)**: `yum install postgresql-devel` - -**OpenSuse**: `zypper in postgresql-devel` - -**ArchLinux**: `pacman -S postgresql-libs` - -##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)*. +The `pg` module has been moved to `db.pg`. +Update your code to do: `import db.pg` instead. diff --git a/vlib/pg/z_deprecated.v b/vlib/pg/z_deprecated.v new file mode 100644 index 0000000000..594326d6a6 --- /dev/null +++ b/vlib/pg/z_deprecated.v @@ -0,0 +1,3 @@ +[deprecated: 'import db.pg instead'] +[deprecated_after: '2023-02-01'] +module pg diff --git a/vlib/sqlite/README.md b/vlib/sqlite/README.md index f111efca92..f6b35963a0 100644 --- a/vlib/sqlite/README.md +++ b/vlib/sqlite/README.md @@ -1,39 +1,4 @@ ## 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 - -Before you can use this module, you must first have the SQLite development -library installed on your system. - -**Fedora 31**: - -`sudo dnf -y install sqlite-devel` - - -**Ubuntu 20.04**: - -`sudo apt install -y libsqlite3-dev` - - -**Windows**: -- Download the source zip from [SQLite Downloads](https://sqlite.org/download.html) -- Create a new `sqlite` subfolder inside `v/thirdparty` -- Extract the zip into that folder - -# 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 sqlite - -db := sqlite.connect('foo.db') or { panic(err) } -db.synchronization_mode(sqlite.SyncMode.off) -db.journal_mode(sqlite.JournalMode.memory) -``` +The `sqlite` module has been moved to `db.sqlite`. +Update your code to do: `import db.sqlite` instead. diff --git a/vlib/sqlite/sqlite.v b/vlib/sqlite/sqlite.v index c9eaeae89d..77e1ca5610 100644 --- a/vlib/sqlite/sqlite.v +++ b/vlib/sqlite/sqlite.v @@ -52,19 +52,19 @@ pub enum JournalMode { memory } -struct C.sqlite3 { +pub struct C.sqlite3 { } -struct C.sqlite3_stmt { +pub struct C.sqlite3_stmt { } [heap] -struct Stmt { +pub struct Stmt { stmt &C.sqlite3_stmt = unsafe { nil } db &DB = unsafe { nil } } -struct SQLError { +pub struct SQLError { MessageError } diff --git a/vlib/sqlite/vfs_lowlevel.v b/vlib/sqlite/vfs_lowlevel.v index 0aa5847dc2..78d5814c82 100644 --- a/vlib/sqlite/vfs_lowlevel.v +++ b/vlib/sqlite/vfs_lowlevel.v @@ -7,14 +7,14 @@ type Sig2 = fn (&Sqlite3_file, &int) int // https://github.com/vlang/v/issues/16 pub type Sqlite3_file = C.sqlite3_file // https://www.sqlite.org/c3ref/file.html -struct C.sqlite3_file { +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] -struct C.sqlite3_io_methods { +pub struct C.sqlite3_io_methods { mut: // version 1 and later fields iVersion int @@ -49,7 +49,7 @@ type Fn_sqlite3_syscall_ptr = fn () pub type Sqlite3_vfs = C.sqlite3_vfs [heap] -struct C.sqlite3_vfs { +pub struct C.sqlite3_vfs { pub mut: // version 1 and later fields iVersion int // Structure version number (currently 3) diff --git a/vlib/sqlite/z_deprecated.v b/vlib/sqlite/z_deprecated.v new file mode 100644 index 0000000000..c44e64b8c8 --- /dev/null +++ b/vlib/sqlite/z_deprecated.v @@ -0,0 +1,7 @@ +[deprecated: 'import db.sqlite instead'] +[deprecated_after: '2023-02-01'] +module sqlite + +import db.sqlite + +const use_sqlite_ok = sqlite.sqlite_ok diff --git a/vlib/v/ast/ast.v b/vlib/v/ast/ast.v index 924b584278..33347fb6ab 100644 --- a/vlib/v/ast/ast.v +++ b/vlib/v/ast/ast.v @@ -1732,11 +1732,12 @@ pub enum SqlStmtKind { pub struct SqlStmt { pub: - pos token.Pos db_expr Expr // `db` in `sql db {` or_expr OrExpr + pos token.Pos pub mut: - lines []SqlStmtLine + lines []SqlStmtLine + db_expr_type Type // the type of the `db` in `sql db {` } pub struct SqlStmtLine { diff --git a/vlib/v/checker/orm.v b/vlib/v/checker/orm.v index 5a0d8e271f..a126d47deb 100644 --- a/vlib/v/checker/orm.v +++ b/vlib/v/checker/orm.v @@ -119,7 +119,7 @@ fn (mut c Checker) sql_expr(mut node ast.SqlExpr) ast.Type { } fn (mut c Checker) sql_stmt(mut node ast.SqlStmt) ast.Type { - c.expr(node.db_expr) + node.db_expr_type = c.table.unaliased_type(c.expr(node.db_expr)) mut typ := ast.void_type for mut line in node.lines { a := c.sql_stmt_line(mut line) diff --git a/vlib/v/checker/tests/orm_empty_struct.vv b/vlib/v/checker/tests/orm_empty_struct.vv index 69aba68b48..1ffbb5e13c 100644 --- a/vlib/v/checker/tests/orm_empty_struct.vv +++ b/vlib/v/checker/tests/orm_empty_struct.vv @@ -1,4 +1,4 @@ -import sqlite +import db.sqlite struct Person { } diff --git a/vlib/v/checker/tests/orm_no_default_value.vv b/vlib/v/checker/tests/orm_no_default_value.vv index 11f15723e3..59f4cf0db3 100644 --- a/vlib/v/checker/tests/orm_no_default_value.vv +++ b/vlib/v/checker/tests/orm_no_default_value.vv @@ -1,4 +1,4 @@ -import sqlite +import db.sqlite struct Person { id int [primary; sql: serial] diff --git a/vlib/v/checker/tests/orm_not_a_struct.vv b/vlib/v/checker/tests/orm_not_a_struct.vv index 9329c951b2..23f83a36b0 100644 --- a/vlib/v/checker/tests/orm_not_a_struct.vv +++ b/vlib/v/checker/tests/orm_not_a_struct.vv @@ -1,4 +1,4 @@ -import sqlite +import db.sqlite enum Person { test diff --git a/vlib/v/checker/tests/orm_using_undefined_var_in_where_err.vv b/vlib/v/checker/tests/orm_using_undefined_var_in_where_err.vv index 590034e4f2..6650ea4e40 100644 --- a/vlib/v/checker/tests/orm_using_undefined_var_in_where_err.vv +++ b/vlib/v/checker/tests/orm_using_undefined_var_in_where_err.vv @@ -1,5 +1,5 @@ import time -import sqlite +import db.sqlite struct User { id int [primary; sql: serial] diff --git a/vlib/v/checker/tests/struct_type_is_private_err.out b/vlib/v/checker/tests/struct_type_is_private_err.out index 6b9f631401..57266567e7 100644 --- a/vlib/v/checker/tests/struct_type_is_private_err.out +++ b/vlib/v/checker/tests/struct_type_is_private_err.out @@ -1,11 +1,11 @@ -vlib/v/checker/tests/struct_type_is_private_err.vv:1:8: warning: module 'sqlite' is imported but never used - 1 | import sqlite - | ~~~~~~ +vlib/v/checker/tests/struct_type_is_private_err.vv:1:8: warning: module 'sqlite (db.sqlite)' is imported but never used + 1 | import db.sqlite + | ~~~~~~~~~ 2 | - 3 | fn main(){ -vlib/v/checker/tests/struct_type_is_private_err.vv:4:10: error: struct `C.sqlite3` was declared as private to module `sqlite`, so it can not be used inside module `main` + 3 | fn main() { +vlib/v/checker/tests/struct_type_is_private_err.vv:4:10: error: struct `C.sqlite3` was declared as private to module `db.sqlite`, so it can not be used inside module `main` 2 | - 3 | fn main(){ + 3 | fn main() { 4 | _ := &C.sqlite3{} | ~~~~~~~~~ 5 | } diff --git a/vlib/v/checker/tests/struct_type_is_private_err.vv b/vlib/v/checker/tests/struct_type_is_private_err.vv index 8cc37e3007..9ed43b7982 100644 --- a/vlib/v/checker/tests/struct_type_is_private_err.vv +++ b/vlib/v/checker/tests/struct_type_is_private_err.vv @@ -1,5 +1,5 @@ -import sqlite +import db.sqlite -fn main(){ +fn main() { _ := &C.sqlite3{} } diff --git a/vlib/v/fmt/tests/orm_keep.vv b/vlib/v/fmt/tests/orm_keep.vv index c4f30c9489..1ce2a3017e 100644 --- a/vlib/v/fmt/tests/orm_keep.vv +++ b/vlib/v/fmt/tests/orm_keep.vv @@ -1,4 +1,4 @@ -import sqlite +import db.sqlite import mymodule { ModDbStruct } struct Customer { diff --git a/vlib/v/fmt/tests/orm_or_keep.vv b/vlib/v/fmt/tests/orm_or_keep.vv index 0e2aa47911..b97644f17d 100644 --- a/vlib/v/fmt/tests/orm_or_keep.vv +++ b/vlib/v/fmt/tests/orm_or_keep.vv @@ -1,4 +1,4 @@ -import sqlite +import db.sqlite struct User { id i64 [primary; sql: serial] diff --git a/vlib/v/gen/c/sql.v b/vlib/v/gen/c/sql.v index 2f6e34c78e..e6565a2f67 100644 --- a/vlib/v/gen/c/sql.v +++ b/vlib/v/gen/c/sql.v @@ -10,38 +10,15 @@ enum SqlExprSide { right } -enum SqlType { - sqlite3 - mysql - psql - mssql - unknown -} - fn (mut g Gen) sql_stmt(node ast.SqlStmt) { conn := g.new_tmp_var() g.writeln('') g.writeln('// orm') g.write('orm__Connection ${conn} = (orm__Connection){._') - mut fn_prefix := '' - typ := g.parse_db_type(node.db_expr) - match typ { - .sqlite3 { - fn_prefix = 'sqlite__DB' - } - .mysql { - fn_prefix = 'mysql__Connection' - } - .psql { - fn_prefix = 'pg__DB' - } - else { - verror('This database type `${typ}` is not implemented yet in orm') // TODO add better error - } - } - g.write('${fn_prefix} = &') + db_expr_ctype_name := g.typ(node.db_expr_type) + g.write('${db_expr_ctype_name} = &') g.expr(node.db_expr) - g.writeln(', ._typ = _orm__Connection_${fn_prefix}_index};') + g.writeln(', ._typ = _orm__Connection_${db_expr_ctype_name}_index};') for line in node.lines { g.sql_stmt_line(line, conn, node.or_expr) } @@ -539,26 +516,13 @@ fn (mut g Gen) sql_select_expr(node ast.SqlExpr) { g.writeln('') g.writeln('// orm') g.write('orm__Connection ${conn} = (orm__Connection){._') - mut fn_prefix := '' - typ := g.parse_db_type(node.db_expr) - match typ { - .sqlite3 { - fn_prefix = 'sqlite__DB' - } - .mysql { - fn_prefix = 'mysql__Connection' - } - .psql { - fn_prefix = 'pg__DB' - } - else { - verror('This database type `${typ}` is not implemented yet in orm') // TODO add better error - } + db_expr_type := g.get_db_type(node.db_expr) or { + verror('sql orm error - unknown db type for ${node.db_expr}') } - - g.write('${fn_prefix} = &') + db_expr_ctype_name := g.typ(db_expr_type) + g.write('${db_expr_ctype_name} = &') g.expr(node.db_expr) - g.writeln(', ._typ = _orm__Connection_${fn_prefix}_index};') + g.writeln(', ._typ = _orm__Connection_${db_expr_ctype_name}_index};') g.sql_select(node, conn, left, node.or_expr) } @@ -831,41 +795,21 @@ fn (mut g Gen) sql_select(node ast.SqlExpr, expr string, left string, or_expr as } } -fn (mut g Gen) parse_db_type(expr ast.Expr) SqlType { +fn (mut g Gen) get_db_type(expr ast.Expr) ?ast.Type { match expr { ast.Ident { if expr.info is ast.IdentVar { - return g.parse_db_from_type_string(g.table.get_final_type_name(expr.info.typ)) + return g.table.unaliased_type(expr.info.typ) } } ast.SelectorExpr { - return g.parse_db_from_type_string(g.table.get_final_type_name(expr.typ)) + return g.table.unaliased_type(expr.typ) } else { - return .unknown - } - } - return .unknown -} - -fn (mut g Gen) parse_db_from_type_string(name string) SqlType { - match name { - 'sqlite.DB' { - return .sqlite3 - } - 'mysql.Connection' { - return .mysql - } - 'pg.DB' { - return .psql - } - 'mssql.Connection' { - return .mssql - } - else { - return .unknown + return none } } + return none } fn (mut g Gen) get_table_name(table_expr ast.TypeNode) string { diff --git a/vlib/v/tests/fn_literal_type_test.v b/vlib/v/tests/fn_literal_type_test.v index 75ec59c409..0d1cb587ab 100644 --- a/vlib/v/tests/fn_literal_type_test.v +++ b/vlib/v/tests/fn_literal_type_test.v @@ -1,4 +1,4 @@ -import sqlite +import db.sqlite [table: 'Users'] struct User { diff --git a/vlib/v/tests/orm_joined_tables_select_test.v b/vlib/v/tests/orm_joined_tables_select_test.v index aa4bbdf6f8..071b25cd4e 100644 --- a/vlib/v/tests/orm_joined_tables_select_test.v +++ b/vlib/v/tests/orm_joined_tables_select_test.v @@ -1,4 +1,4 @@ -import sqlite +import db.sqlite struct VieterDb { conn sqlite.DB diff --git a/vlib/v/tests/orm_sub_array_struct_test.v b/vlib/v/tests/orm_sub_array_struct_test.v index 6643dcf4ed..999fde2593 100644 --- a/vlib/v/tests/orm_sub_array_struct_test.v +++ b/vlib/v/tests/orm_sub_array_struct_test.v @@ -1,4 +1,4 @@ -import sqlite +import db.sqlite struct Parent { id int [primary; sql: serial] diff --git a/vlib/v/tests/orm_sub_struct_test.v b/vlib/v/tests/orm_sub_struct_test.v index ef26eb817f..d550b6c8b1 100644 --- a/vlib/v/tests/orm_sub_struct_test.v +++ b/vlib/v/tests/orm_sub_struct_test.v @@ -1,4 +1,4 @@ -import sqlite +import db.sqlite struct Upper { id int [primary; sql: serial] diff --git a/vlib/v/tests/sql_statement_inside_fn_call_test.v b/vlib/v/tests/sql_statement_inside_fn_call_test.v index 536585dc3e..ba5410bfbf 100644 --- a/vlib/v/tests/sql_statement_inside_fn_call_test.v +++ b/vlib/v/tests/sql_statement_inside_fn_call_test.v @@ -1,4 +1,4 @@ -import sqlite +import db.sqlite struct Movie { id int [primary] diff --git a/vlib/vweb/vweb_app_test.v b/vlib/vweb/vweb_app_test.v index 1db4755e7c..941e0016ce 100644 --- a/vlib/vweb/vweb_app_test.v +++ b/vlib/vweb/vweb_app_test.v @@ -2,7 +2,7 @@ module main import vweb import time -import sqlite +import db.sqlite struct App { vweb.Context