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 }