1 // NOTE: I haven't even tried to use this for a test yet! 2 // It's probably godawful, if it works at all. 3 /++ 4 Implementation of [arsd.database.Database] interface for 5 Microsoft SQL Server (among others), via ODBC. 6 7 On Unix, needs the `unixodbc` library. 8 9 History: 10 Originally written November 9, 2011 11 12 Unix support added December 11, 2025. 13 +/ 14 module arsd.mssql; 15 16 version(Windows) 17 pragma(lib, "odbc32"); 18 else 19 pragma(lib, "odbc"); 20 21 public import arsd.database; 22 23 import std.string; 24 import std.exception; 25 26 import etc.c.odbc.sql; 27 import etc.c.odbc.sqlext; 28 29 //import core.sys.windows.sql; 30 //import core.sys.windows.sqlext; 31 32 /// 33 class MsSql : Database { 34 /// auto db = new MsSql("Driver={SQL Server Native Client 10.0};Server=<host>[\\<optional-instance-name>];Database=dbtest;Trusted_Connection=Yes") 35 this(string connectionString) { 36 SQLAllocHandle(SQL_HANDLE_ENV, cast(void*)SQL_NULL_HANDLE, &env); 37 enforce(env !is null); 38 scope(failure) 39 SQLFreeHandle(SQL_HANDLE_ENV, env); 40 SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, cast(void *) SQL_OV_ODBC3, 0); 41 SQLAllocHandle(SQL_HANDLE_DBC, env, &conn); 42 scope(failure) 43 SQLFreeHandle(SQL_HANDLE_DBC, conn); 44 enforce(conn !is null); 45 46 auto ret = SQLDriverConnect( 47 conn, null, cast(ubyte*)connectionString.ptr, SQL_NTS, 48 null, 0, null, 49 SQL_DRIVER_NOPROMPT ); 50 51 if ((ret != SQL_SUCCESS_WITH_INFO) && (ret != SQL_SUCCESS)) 52 throw new DatabaseConnectionException("Unable to connect to ODBC object: " ~ getSQLError(SQL_HANDLE_DBC, conn)); // FIXME: print error 53 54 //query("SET NAMES 'utf8'"); // D does everything with utf8 55 } 56 57 ~this() { 58 SQLDisconnect(conn); 59 SQLFreeHandle(SQL_HANDLE_DBC, conn); 60 SQLFreeHandle(SQL_HANDLE_ENV, env); 61 } 62 63 override void startTransaction() { 64 query("START TRANSACTION"); 65 } 66 67 // possible fixme, idk if this is right 68 override string sysTimeToValue(SysTime s) { 69 return "'" ~ escape(s.toISOExtString()) ~ "'"; 70 } 71 72 ResultSet queryImpl(string sql, Variant[] args...) { 73 sql = escapedVariants(this, sql, args); 74 75 // this is passed to MsSqlResult to control 76 SQLHSTMT statement; 77 auto returned = SQLAllocHandle(SQL_HANDLE_STMT, conn, &statement); 78 79 enforce(returned == SQL_SUCCESS); 80 81 returned = SQLExecDirect(statement, cast(ubyte*)sql.ptr, cast(SQLINTEGER) sql.length); 82 if(returned != SQL_SUCCESS) 83 throw new SqlException(getSQLError(SQL_HANDLE_STMT, statement)); 84 85 return new MsSqlResult(statement); 86 } 87 88 string escape(string sqlData) { // FIXME 89 return ""; //FIX ME 90 //return ret.replace("'", "''"); 91 } 92 93 string escapeBinaryString(const(ubyte)[] data) { // FIXME 94 return "'" ~ escape(cast(string) data) ~ "'"; 95 } 96 97 98 string error() { 99 return null; // FIXME 100 } 101 102 override bool isAlive() { 103 return true; 104 } 105 106 private: 107 SQLHENV env; 108 SQLHDBC conn; 109 } 110 111 class MsSqlResult : ResultSet { 112 // name for associative array to result index 113 int getFieldIndex(string field) { 114 if(mapping is null) 115 makeFieldMapping(); 116 if (field !in mapping) 117 return -1; 118 return mapping[field]; 119 } 120 121 122 string[] fieldNames() { 123 if(mapping is null) 124 makeFieldMapping(); 125 return columnNames; 126 } 127 128 // this is a range that can offer other ranges to access it 129 bool empty() { 130 return isEmpty; 131 } 132 133 Row front() { 134 return row; 135 } 136 137 void popFront() { 138 if(!isEmpty) 139 fetchNext; 140 } 141 142 override size_t length() 143 { 144 return 1; //FIX ME 145 } 146 147 this(SQLHSTMT statement) { 148 this.statement = statement; 149 150 SQLSMALLINT info; 151 SQLNumResultCols(statement, &info); 152 numFields = info; 153 154 fetchNext(); 155 } 156 157 ~this() { 158 SQLFreeHandle(SQL_HANDLE_STMT, statement); 159 } 160 161 private: 162 SQLHSTMT statement; 163 int[string] mapping; 164 string[] columnNames; 165 int numFields; 166 167 bool isEmpty; 168 169 Row row; 170 171 void fetchNext() { 172 if(isEmpty) 173 return; 174 175 if(SQLFetch(statement) == SQL_SUCCESS) { 176 Row r; 177 r.resultSet = this; 178 DatabaseDatum[] row; 179 180 for(int i = 0; i < numFields; i++) { 181 string a; 182 183 SQLLEN ptr; 184 185 more: 186 SQLCHAR[1024] buf; 187 if(SQLGetData(statement, cast(ushort)(i+1), SQL_CHAR, buf.ptr, 1024, &ptr) != SQL_SUCCESS) 188 throw new DatabaseException("get data: " ~ getSQLError(SQL_HANDLE_STMT, statement)); 189 190 assert(ptr != SQL_NO_TOTAL); 191 if(ptr == SQL_NULL_DATA) 192 a = null; 193 else { 194 a ~= cast(string) buf[0 .. ptr > 1024 ? 1024 : ptr].idup; 195 ptr -= ptr > 1024 ? 1024 : ptr; 196 if(ptr) 197 goto more; 198 } 199 row ~= DatabaseDatum(a); 200 } 201 202 r.row = row; 203 this.row = r; 204 } else { 205 isEmpty = true; 206 } 207 } 208 209 void makeFieldMapping() { 210 for(int i = 0; i < numFields; i++) { 211 SQLSMALLINT len; 212 SQLCHAR[1024] buf; 213 auto ret = SQLDescribeCol(statement, 214 cast(ushort)(i+1), 215 cast(ubyte*)buf.ptr, 216 1024, 217 &len, 218 null, null, null, null); 219 if (ret != SQL_SUCCESS) 220 throw new DatabaseException("Field mapping error: " ~ getSQLError(SQL_HANDLE_STMT, statement)); 221 222 string a = cast(string) buf[0 .. len].idup; 223 224 columnNames ~= a; 225 mapping[a] = i; 226 } 227 228 } 229 } 230 231 private string getSQLError(short handletype, SQLHANDLE handle) 232 { 233 char[32] sqlstate; 234 char[256] message; 235 SQLINTEGER nativeerror=0; 236 SQLSMALLINT textlen=0; 237 auto ret = SQLGetDiagRec(handletype, handle, 1, 238 cast(ubyte*)sqlstate.ptr, 239 cast(int*)&nativeerror, 240 cast(ubyte*)message.ptr, 241 256, 242 &textlen); 243 244 return message[0 .. textlen].idup; 245 } 246 247 /* 248 import std.stdio; 249 void main() { 250 //auto db = new MsSql("Driver={SQL Server};Server=<host>[\\<optional-instance-name>]>;Database=dbtest;Trusted_Connection=Yes"); 251 auto db = new MsSql("Driver={SQL Server Native Client 10.0};Server=<host>[\\<optional-instance-name>];Database=dbtest;Trusted_Connection=Yes") 252 253 db.query("INSERT INTO users (id, name) values (30, 'hello mang')"); 254 255 foreach(line; db.query("SELECT * FROM users")) { 256 writeln(line[0], line["name"]); 257 } 258 } 259 */ 260 261 void omg() { 262 263 enum EMPLOYEE_ID_LEN = 6 ; 264 265 SQLHENV henv = null; 266 SQLHDBC hdbc = null; 267 SQLRETURN retcode; 268 SQLHSTMT hstmt = null; 269 SQLSMALLINT sCustID; 270 271 SQLCHAR[EMPLOYEE_ID_LEN]szEmployeeID; 272 SQL_DATE_STRUCT dsOrderDate; 273 SQLINTEGER cbCustID = 0, cbOrderDate = 0, cbEmployeeID = SQL_NTS; 274 275 retcode = SQLAllocHandle(SQL_HANDLE_ENV, cast(void*) SQL_NULL_HANDLE, &henv); 276 retcode = SQLSetEnvAttr(henv, SQL_ATTR_ODBC_VERSION, cast(SQLPOINTER*)SQL_OV_ODBC3, 0); 277 278 retcode = SQLAllocHandle(SQL_HANDLE_DBC, henv, &hdbc); 279 retcode = SQLSetConnectAttr(hdbc, SQL_LOGIN_TIMEOUT, cast(SQLPOINTER)5, 0); 280 281 retcode = SQLDriverConnect( 282 hdbc, null, cast(ubyte*)"DSN=PostgreSQL30Postgres".ptr, SQL_NTS, 283 null, 0, null, 284 SQL_DRIVER_NOPROMPT ); 285 286 287 import std.stdio; writeln(retcode); 288 retcode = SQLAllocHandle(SQL_HANDLE_STMT, hdbc, &hstmt); 289 290 szEmployeeID[0 .. 6] = cast(ubyte[]) "BERGS\0"; 291 292 sCustID = 5; 293 dsOrderDate.year = 2006; 294 dsOrderDate.month = 3; 295 dsOrderDate.day = 17; 296 297 298 /* 299 retcode = SQLBindParameter(hstmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_CHAR, EMPLOYEE_ID_LEN, 0, szEmployeeID.ptr, 0, &cbEmployeeID); 300 import std.stdio; writeln(retcode); writeln(getSQLError(SQL_HANDLE_STMT, hstmt)); 301 */ 302 retcode = SQLBindParameter(hstmt, 1, SQL_PARAM_INPUT, SQL_C_SSHORT, SQL_INTEGER, 0, 0, &sCustID, 0, &cbCustID); 303 import std.stdio; writeln(retcode); writeln(getSQLError(SQL_HANDLE_STMT, hstmt)); 304 /* 305 retcode = SQLBindParameter(hstmt, 3, SQL_PARAM_INPUT, SQL_C_TYPE_DATE, SQL_TIMESTAMP, dsOrderDate.sizeof, 0, &dsOrderDate, 0, &cbOrderDate); 306 import std.stdio; writeln(retcode); writeln(getSQLError(SQL_HANDLE_STMT, hstmt)); 307 */ 308 309 retcode = SQLPrepare(hstmt, cast(SQLCHAR*)"INSERT INTO Orders(CustomerID, EmployeeID, OrderDate) VALUES ('omg', ?, 'now')", SQL_NTS); 310 311 import std.stdio; writeln("here ", retcode); writeln(getSQLError(SQL_HANDLE_STMT, hstmt)); 312 313 retcode = SQLExecute(hstmt); 314 import std.stdio; writeln(retcode); writeln(getSQLError(SQL_HANDLE_STMT, hstmt)); 315 }