1 module monetdb.monetdb; 2 3 import std.string; 4 import std.exception; 5 import std.conv; 6 import std.datetime; 7 import std.range; 8 import std.variant; 9 10 import monetdb.binding; 11 12 alias Null = typeof(null); 13 14 //TODO: support const type as well 15 alias Record = Algebraic!(int, short, long, double, char, bool, string, Date, DateTime, Null); 16 17 class MonetDbException : Exception 18 { 19 this(string msg, string file = __FILE__, size_t line = __LINE__) 20 { 21 super(msg, file, line); 22 } 23 } 24 25 protected Record dize(T)(const T typeName, const T value) if (is(T : string)) 26 { 27 Record res; 28 if (value == "") 29 { 30 res = null; 31 return res; 32 } 33 if (typeName == "int") 34 { 35 res = value.to!int; 36 } 37 else if (typeName == "tinyint") 38 { 39 res = value.to!short; 40 } 41 else if (typeName == "bigint") 42 { 43 res = value.to!long; 44 } 45 else if (typeName == "double") 46 { 47 res = value.to!double; 48 } 49 else if (typeName == "date") 50 { 51 res = Date.fromISOExtString(value.to!string); 52 } 53 else if (typeName == "timestamp") 54 { 55 auto toExtISOFormat = (const string d) => d[0 .. 10] ~ "T" ~ d[11 .. 19]; 56 res = DateTime.fromISOExtString(toExtISOFormat(value.to!string)); 57 } 58 else if (typeName == "varchar" || typeName == "char") 59 { 60 res = value.to!string; 61 } 62 else 63 { 64 enforce!MonetDbException(false, "Could not d-ize type: " ~ typeName); 65 } 66 return res; 67 } 68 69 protected MapiDate monetizeDate(const Date date) 70 { 71 return MapiDate(date.year, date.month, date.day); 72 } 73 74 protected MapiDateTime monetizeDateTime(const DateTime date) 75 { 76 return MapiDateTime(date.year, date.month, date.day, date.hour, date.minute, date.second, 0); 77 } 78 79 Record[] recordArray(T...)(T params) 80 { 81 Record[] records; 82 foreach (i, p; params) 83 { 84 records ~= Record(p); 85 } 86 return records; 87 } 88 89 class MonetDb 90 { 91 private Mapi _mapi; 92 93 this(string host, int port, string username, string password, string lang, string dbname) @trusted 94 { 95 _mapi = mapi_connect(toStringz(host), port, toStringz(username), 96 toStringz(password), toStringz(lang), toStringz(dbname)); 97 } 98 99 ~this() 100 { 101 mapi_destroy(_mapi); 102 } 103 104 private string errorMessage(MapiHdl handler) 105 { 106 auto msg = mapi_error_str(_mapi); 107 if (msg) 108 { 109 return to!string(msg); 110 } 111 return to!string(mapi_result_error(handler)); 112 } 113 114 private int getMapiType(Record p) 115 { 116 return p.visit!((int a) => MAPI_INT, (short a) => MAPI_SHORT, 117 (long a) => MAPI_LONG, (char a) => MAPI_CHAR, (string a) => MAPI_VARCHAR, 118 (double a) => MAPI_DOUBLE, (Date a) => MAPI_DATE, 119 (DateTime a) => MAPI_DATETIME, (bool a) => MAPI_USHORT, (Null a) => -1)(); 120 } 121 122 private MapiHdl buildHandler(string command, Record[] params) 123 { 124 MapiHdl result; 125 if (params is null) 126 { 127 result = mapi_query(_mapi, toStringz(command)); 128 } 129 else 130 { 131 result = mapi_prepare(_mapi, toStringz(command)); 132 foreach (i, p; params) 133 { 134 auto mapiType = getMapiType(p); 135 enforce!MonetDbException(mapiType >= 0, 136 "Parameter having a null value are not supported!"); 137 if (p.type == typeid(Date)) 138 { 139 auto d = p.get!Date.monetizeDate; 140 mapi_param_type(result, i.to!int, mapiType, mapiType, &d); 141 } 142 else if (p.type == typeid(DateTime)) 143 { 144 auto d = p.get!DateTime.monetizeDateTime; 145 mapi_param_type(result, i.to!int, mapiType, mapiType, &d); 146 } 147 else if (p.type == typeid(string)) 148 { 149 auto s = cast(char*) p.get!string.toStringz; 150 mapi_param_type(result, i.to!int, mapiType, mapiType, s); 151 } 152 else if (p.type == typeid(int)) 153 { 154 auto v = p.get!int; 155 mapi_param_type(result, i.to!int, mapiType, mapiType, &v); 156 } 157 else if (p.type == typeid(long)) 158 { 159 auto v = p.get!long; 160 mapi_param_type(result, i.to!int, mapiType, mapiType, &v); 161 } 162 else if (p.type == typeid(double)) 163 { 164 auto v = p.get!double; 165 mapi_param_type(result, i.to!int, mapiType, mapiType, &v); 166 } 167 else if (p.type == typeid(char)) 168 { 169 auto v = p.get!char; 170 mapi_param_type(result, i.to!int, mapiType, mapiType, &v); 171 } 172 else if (p.type == typeid(bool)) 173 { 174 auto v = p.get!bool; 175 mapi_param_type(result, i.to!int, mapiType, mapiType, &v); 176 } 177 else 178 { 179 assert(false); 180 } 181 } 182 mapi_execute(result); 183 } 184 enforce!MonetDbException(mapi_error(_mapi) == MOK, errorMessage(result)); 185 186 return result; 187 } 188 189 void exec(string command, Record[] params = null) 190 { 191 auto result = buildHandler(command, params); 192 mapi_close_handle(result); 193 } 194 195 auto query(string command, Record[] params = null) 196 { 197 auto result = buildHandler(command, params); 198 199 static struct QueryResult 200 { 201 private MapiHdl handler_; 202 private bool hasRow_; 203 private int count_; 204 205 this(MapiHdl handler) 206 { 207 handler_ = handler; 208 fetchRow(); 209 } 210 211 auto empty() 212 { 213 return !hasRow_; 214 } 215 216 private void fetchRow() 217 { 218 hasRow_ = mapi_fetch_row(handler_) != 0; 219 count_ = mapi_get_field_count(handler_); 220 } 221 222 auto front() 223 { 224 assert(!empty); 225 226 Record[string] records; 227 foreach (i; iota(count_)) 228 { 229 auto name = mapi_get_name(handler_, i).to!string; 230 auto typeName = mapi_get_type(handler_, i).to!string; 231 auto value = mapi_fetch_field(handler_, i).to!string; 232 records[name] = dize(typeName, value); 233 } 234 return records; 235 } 236 237 void popFront() 238 { 239 assert(!empty); 240 fetchRow(); 241 if (!hasRow_) 242 mapi_close_handle(handler_); 243 } 244 } 245 246 return QueryResult(result); 247 } 248 249 void close() 250 { 251 mapi_disconnect(_mapi); 252 } 253 } 254 255 unittest 256 { 257 auto conn = new MonetDb("localhost", 50_000, "monetdb", "monetdb", "sql", "16megabytes"); 258 scope (exit) 259 conn.close(); 260 261 conn.exec("DROP TABLE IF EXISTS FOO;"); 262 conn.exec(`CREATE TABLE IF NOT EXISTS FOO ( 263 ID INTEGER NOT NULL, 264 VALUE VARCHAR(5), 265 RATIO DOUBLE, 266 CREATION DATE, 267 SYSDATE TIMESTAMP DEFAULT NOW, 268 FLAG BOOLEAN DEFAULT FALSE, 269 UNIT CHAR DEFAULT 'U' 270 );`); 271 conn.exec("INSERT INTO FOO (ID, VALUE, RATIO, CREATION) VALUES (1, 'foo', .5, '2018-01-01');"); 272 Record[] p1 = recordArray(2, "bar", .63, Date(2018, 1, 1)); 273 conn.exec("INSERT INTO FOO (ID, VALUE, RATIO, CREATION) VALUES (?, ?, ?, ?);", p1); 274 Record[] p2 = recordArray(3, Date(2019, 3, 31)); 275 conn.exec("INSERT INTO FOO (ID, CREATION) VALUES (?, ?)", p2); 276 277 auto r = conn.query("SELECT ID, VALUE, CREATION, SYSDATE, RATIO FROM FOO ORDER BY ID"); 278 auto row = r.front; 279 assert("id" in row); 280 assert("value" in row); 281 assert("creation" in row); 282 assert(row["id"].get!int == 1); 283 assert(row["value"].get!string == "foo"); 284 assert(row["creation"].get!Date == Date(2018, 1, 1)); 285 r.popFront(); 286 auto row2 = r.front; 287 assert(row2["id"].get!int == 2); 288 assert(row2["value"].get!string == "bar"); 289 assert(row2["creation"].get!Date == Date(2018, 1, 1)); 290 r.popFront(); 291 auto row3 = r.front; 292 assert(row3["value"].get!Null is null); 293 assert(row3["ratio"].get!Null is null); 294 295 Record[] params = recordArray(1, "foo"); 296 auto rp = conn.query( 297 "SELECT ID, VALUE, RATIO, CREATION FROM FOO WHERE ID = ? AND VALUE = ?", params); 298 assert(rp.front["id"].get!int == 1); 299 assert(rp.front["value"].get!string == "foo"); 300 assert(rp.front["ratio"].get!double == .5); 301 assert(rp.front["creation"].get!Date == Date(2018, 1, 1)); 302 303 auto rl = conn.query("SELECT COUNT(*) AS V, ID FROM FOO WHERE FLAG = ? AND SYSDATE > ? AND UNIT = ? GROUP BY ID HAVING COUNT(*) = ?;", 304 recordArray(false, DateTime(1970, 1, 1, 0, 0, 0), 'U', 1L)); 305 assert(rl.front["v"].get!long == 1L); 306 307 conn.exec("DROP TABLE FOO;"); 308 } 309 310 unittest 311 { 312 auto conn = new MonetDb("localhost", 50_000, "monetdb", "monetdb", "sql", "16megabytes"); 313 scope (exit) 314 conn.close(); 315 316 conn.exec( 317 "CREATE TABLE IF NOT EXISTS FOO (ID INT); INSERT INTO FOO (ID) VALUES (1), (2); DROP TABLE FOO;"); 318 }