From d9ab8a91f033ccf7938755d23caf244e098765a4 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Fri, 24 Apr 2026 01:22:15 +0300 Subject: [PATCH] db: fix V support to Microsoft SQL Server or to ODBC connection (fixes #4531) --- vlib/db/mssql/README.md | 62 ++++++++++++++++++---------- vlib/db/mssql/config.v | 84 +++++++++++++++++++++++++++++++++----- vlib/db/mssql/mssql_test.v | 40 +++++++++++++++--- vlib/db/mssql/result.v | 10 +++++ 4 files changed, 158 insertions(+), 38 deletions(-) diff --git a/vlib/db/mssql/README.md b/vlib/db/mssql/README.md index 65d68de19..f34b0fa58 100644 --- a/vlib/db/mssql/README.md +++ b/vlib/db/mssql/README.md @@ -1,13 +1,14 @@ # SQL Server ODBC * This module wraps the ODBC C API for use with SQL Server. +* It can also be used with any ODBC data source by passing a raw ODBC connection string. ## Scope -* `db.mssql` can connect to any ODBC data source when `Connection.connect(...)` receives a valid - ODBC connection string. -* `Config` is a convenience helper for SQL Server fields (`Driver`, `Server`, `UID`, `PWD`, - `Database`). -* For non-MSSQL ODBC sources, pass a DSN/raw ODBC string directly. +* `db.mssql` can connect to any ODBC data source when `mssql.open(...)` or + `Connection.connect(...)` receives a valid ODBC connection string. +* `Config` can build connection strings from `driver`, `server`, `port`, `uid`/`user`, + `pwd`/`password`, `dbname`, `dsn`, and extra ODBC options. +* For non-MSSQL ODBC sources, pass a DSN or raw ODBC string directly. ## Dependencies * ODBC driver manager development headers/libraries (`sql.h`, `sqlext.h`). @@ -17,8 +18,7 @@ * Recommended: install unixODBC + pkg-config: `brew install unixodbc pkg-config` * Then install your DB vendor ODBC driver (for SQL Server: `msodbcsql18`). - * Details: - https://learn.microsoft.com/en-us/sql/connect/odbc/linux-mac/installing-the-microsoft-odbc-driver-for-sql-server + * Details: see the Microsoft SQL Server ODBC driver installation guide. * Windows: * `odbc32` is included with the Windows SDK on most systems. * Details: @@ -35,11 +35,21 @@ 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 +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` @@ -52,16 +62,18 @@ import db.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())? + mut conn := mssql.connect( + driver: 'ODBC Driver 18 for SQL Server' + server: '127.0.0.1' + port: 1433 + user: '' + password: '' + dbname: 'master' + options: { + 'Encrypt': 'yes' + 'TrustServerCertificate': 'yes' + } + )? defer { conn.close() @@ -78,3 +90,9 @@ fn test_example() ? { } } ``` + +You can also connect with a raw DSN or ODBC string: + +```v ignore +mut conn := mssql.open('DSN=Reporting;Trusted_Connection=Yes')? +``` diff --git a/vlib/db/mssql/config.v b/vlib/db/mssql/config.v index 09652e436..4b71abd30 100644 --- a/vlib/db/mssql/config.v +++ b/vlib/db/mssql/config.v @@ -1,22 +1,86 @@ module mssql -// Config TODO +@[params] pub struct Config { pub: - driver string - server string - uid string - pwd string + conn_str string + dsn string + driver string + server string + port int + uid string + user string + pwd string + password string // if dbname empty, conn str will not contain Database info, // and it is up to the server to choose which db to connect to. - dbname string + dbname string + options map[string]string } -// get_conn_str TODO +// connect creates a new connection using the provided configuration. +pub fn connect(config Config) !Connection { + return open(config.get_conn_str()) +} + +// open creates a new connection using a raw ODBC connection string. +pub fn open(conn_str string) !Connection { + mut conn := Connection{} + conn.connect(conn_str)! + return conn +} + +fn preferred_value(primary string, fallback string) string { + if primary != '' { + return primary + } + return fallback +} + +fn needs_odbc_braces(value string) bool { + return value.contains_any(' \t\r\n;{}') +} + +fn format_odbc_value(value string) string { + if value == '' { + return '' + } + if needs_odbc_braces(value) { + return '{${value.replace('}', '}}')}' + } + return value +} + +fn append_conn_part(mut parts []string, key string, value string) { + if value == '' { + return + } + parts << '${key}=${format_odbc_value(value)}' +} + +// get_conn_str builds an ODBC connection string from the configured fields. pub fn (cfg Config) get_conn_str() string { - mut str := 'Driver=${cfg.driver};Server=${cfg.server};UID=${cfg.uid};PWD=${cfg.pwd}' + if cfg.conn_str != '' { + return cfg.conn_str + } + mut parts := []string{} + append_conn_part(mut parts, 'DSN', cfg.dsn) + append_conn_part(mut parts, 'Driver', cfg.driver) + append_conn_part(mut parts, 'Server', cfg.server) + if cfg.port > 0 { + parts << 'Port=${cfg.port}' + } + append_conn_part(mut parts, 'UID', preferred_value(cfg.uid, cfg.user)) + append_conn_part(mut parts, 'PWD', preferred_value(cfg.pwd, cfg.password)) if cfg.dbname != '' { - str += ';Database=${cfg.dbname}' + append_conn_part(mut parts, 'Database', cfg.dbname) + } + if cfg.options.len > 0 { + mut option_keys := cfg.options.keys() + option_keys.sort() + for key in option_keys { + append_conn_part(mut parts, key, cfg.options[key]) + } } - return str + return parts.join(';') } diff --git a/vlib/db/mssql/mssql_test.v b/vlib/db/mssql/mssql_test.v index 41f0c113e..4eb2fc979 100644 --- a/vlib/db/mssql/mssql_test.v +++ b/vlib/db/mssql/mssql_test.v @@ -14,13 +14,41 @@ fn test_config_get_conn_str() { uid: 'sa' pwd: 'secret' dbname: 'master' - }.get_conn_str() == 'Driver=ODBC Driver 18 for SQL Server;Server=tcp:localhost;UID=sa;PWD=secret;Database=master' + }.get_conn_str() == 'Driver={ODBC Driver 18 for SQL Server};Server=tcp:localhost;UID=sa;PWD=secret;Database=master' assert mssql.Config{ - driver: 'FreeTDS' - server: '127.0.0.1' - uid: 'sa' - pwd: 'secret' - }.get_conn_str() == 'Driver=FreeTDS;Server=127.0.0.1;UID=sa;PWD=secret' + driver: 'FreeTDS' + server: '127.0.0.1' + port: 1433 + user: 'sa' + password: 'secret' + options: { + 'ClientCharset': 'UTF-8' + 'TDS_Version': '7.4' + } + }.get_conn_str() == 'Driver=FreeTDS;Server=127.0.0.1;Port=1433;UID=sa;PWD=secret;ClientCharset=UTF-8;TDS_Version=7.4' + assert mssql.Config{ + dsn: 'Reporting DB' + user: 'sa' + password: 'secret' + dbname: 'master' + options: { + 'Encrypt': 'yes' + 'TrustServerCertificate': 'yes' + } + }.get_conn_str() == 'DSN={Reporting DB};UID=sa;PWD=secret;Database=master;Encrypt=yes;TrustServerCertificate=yes' + assert mssql.Config{ + conn_str: 'DSN=Accounting;Trusted_Connection=Yes' + driver: 'ignored' + }.get_conn_str() == 'DSN=Accounting;Trusted_Connection=Yes' +} + +fn test_row_helpers() { + row := mssql.Row{ + vals: ['1', 'alice'] + } + assert row.val(0) == '1' + assert row.val(1) == 'alice' + assert row.values() == ['1', 'alice'] } fn test_connection_and_query() { diff --git a/vlib/db/mssql/result.v b/vlib/db/mssql/result.v index f5483a285..667abc45d 100644 --- a/vlib/db/mssql/result.v +++ b/vlib/db/mssql/result.v @@ -5,6 +5,16 @@ pub mut: vals []string } +// val returns the value at `index`. +pub fn (row Row) val(index int) string { + return row.vals[index] +} + +// values returns all row values. +pub fn (row Row) values() []string { + return row.vals.clone() +} + pub struct Result { pub mut: rows []Row -- 2.39.5