1 /++ 2 Implementation of the [arsd.database.Database] interface for 3 accessing MySQL (and MariaDB) databases. Uses the official MySQL client 4 library, and thus needs that installed to compile and run. 5 6 $(PITFALL 7 If you're using MySQL client library v5.0 or less, 8 you must pass this to dmd: `-version=Less_Than_MySQL_51` 9 This is important - otherwise you will see bizarre segfaults! 10 ) 11 +/ 12 module arsd.mysql; 13 14 15 //version(MySQL_51) { 16 // we good 17 /*} else*/ version(Less_Than_MySQL_51) { 18 // we good 19 } else { 20 // default now is mysql 5.1 or up - it has been long 21 // enough that surely most everyone uses it and we don't 22 // need the pragma warning anymore. Of course, the old is 23 // still available if you need to explicitly opt in. 24 version = MySQL_51; 25 } 26 27 version(Windows) { 28 pragma(lib, "libmysql"); 29 } 30 else { 31 pragma(lib, "mysqlclient"); 32 } 33 34 public import arsd.database; 35 36 import std.stdio; 37 import std.exception; 38 import std.string; 39 import std.conv; 40 import std.typecons; 41 import core.stdc.config; 42 43 /++ 44 Represents a query result. You can loop over this with a 45 `foreach` statement to access individual [Row|rows]. 46 47 [Row]s expose both an index and associative array interface, 48 so you can get `row[0]` for the first item, or `row["name"]` 49 to get a column by name from the result set. 50 +/ 51 class MySqlResult : ResultSet { 52 private int[string] mapping; 53 private MYSQL_RES* result; 54 55 private int itemsTotal; 56 private int itemsUsed; 57 58 string sql; 59 60 this(MYSQL_RES* r, string sql) { 61 result = r; 62 itemsTotal = cast(int) length(); 63 itemsUsed = 0; 64 65 this.sql = sql; 66 67 // prime it 68 if(itemsTotal) 69 fetchNext(); 70 } 71 72 ~this() { 73 if(result !is null) 74 mysql_free_result(result); 75 } 76 77 78 MYSQL_FIELD[] fields() @system { 79 int numFields = mysql_num_fields(result); 80 auto fields = mysql_fetch_fields(result); 81 82 MYSQL_FIELD[] ret; 83 for(int i = 0; i < numFields; i++) { 84 ret ~= fields[i]; 85 } 86 87 return ret; 88 } 89 90 91 /// The number of returned rows 92 override size_t length() { 93 if(result is null) 94 return 0; 95 return cast(int) mysql_num_rows(result); 96 } 97 98 /// Range primitive used by `foreach` 99 /// You may also use this to check if there was any result. 100 override bool empty() { 101 return itemsUsed == itemsTotal; 102 } 103 104 /// Range primitive used by `foreach` 105 override Row front() { 106 return row; 107 } 108 109 /// Range primitive used by `foreach` 110 override void popFront() { 111 itemsUsed++; 112 if(itemsUsed < itemsTotal) { 113 fetchNext(); 114 } 115 } 116 117 override int getFieldIndex(string field) { 118 if(mapping is null) 119 makeFieldMapping(); 120 debug { 121 if(field !in mapping) 122 throw new Exception(field ~ " not in result"); 123 } 124 return mapping[field]; 125 } 126 127 private void makeFieldMapping() @system { 128 int numFields = mysql_num_fields(result); 129 auto fields = mysql_fetch_fields(result); 130 131 if(fields is null) 132 return; 133 134 for(int i = 0; i < numFields; i++) { 135 if(fields[i].name !is null) 136 mapping[fromCstring(fields[i].name, fields[i].name_length)] = i; 137 } 138 } 139 140 private void fetchNext() @system { 141 assert(result); 142 auto r = mysql_fetch_row(result); 143 if(r is null) 144 throw new Exception("there is no next row"); 145 uint numFields = mysql_num_fields(result); 146 auto lengths = mysql_fetch_lengths(result); 147 DatabaseDatum[] row; 148 // potential FIXME: not really binary safe 149 150 columnIsNull.length = numFields; 151 for(int a = 0; a < numFields; a++) { 152 if(*(r+a) is null) { 153 row ~= DatabaseDatum(null); 154 columnIsNull[a] = true; 155 } else { 156 row ~= DatabaseDatum(fromCstring(*(r+a), *(lengths + a))); 157 columnIsNull[a] = false; 158 } 159 } 160 161 this.row.row = row; 162 this.row.resultSet = this; 163 } 164 165 166 override string[] fieldNames() @system { 167 int numFields = mysql_num_fields(result); 168 auto fields = mysql_fetch_fields(result); 169 170 string[] names; 171 for(int i = 0; i < numFields; i++) { 172 names ~= fromCstring(fields[i].name, fields[i].name_length); 173 } 174 175 return names; 176 } 177 178 179 180 bool[] columnIsNull; 181 Row row; 182 } 183 184 /++ 185 The main class for accessing the MySql database. 186 187 --- 188 // connect to database with the constructor 189 auto db = new MySql("localhost", "my_user", "my_password", "my_database_name"); 190 // use the query function to execute sql... 191 // use ? for data placeholders... 192 db.query("INSERT INTO people (id, name) VALUES (?, ?)", 10, "My Name"); 193 // and use foreach to loop over result sets 194 foreach(row; db.query("SELECT id, name FROM people ORDER BY name LIMIT 10")) 195 writeln(row[0], " ", row["name"]); // index and name supported 196 --- 197 +/ 198 class MySql : Database { 199 this(string host, string user, string pass, string db, uint port = 0) { 200 mysql = enforce!(DatabaseException)( 201 mysql_init(null), 202 "Couldn't init mysql"); 203 enforce!(DatabaseConnectionException)( 204 mysql_real_connect(mysql, toCstring(host), toCstring(user), toCstring(pass), toCstring(db), port, null, 0), 205 error()); 206 207 dbname = db; 208 209 // we want UTF8 for everything 210 211 query("SET NAMES 'utf8mb4'"); 212 //query("SET CHARACTER SET utf8mb4"); 213 } 214 215 string dbname; 216 217 /// 218 override void startTransaction() { 219 query("START TRANSACTION"); 220 } 221 222 override bool isAlive() { 223 return true; 224 } 225 226 string sysTimeToValue(SysTime s) { 227 return "cast('" ~ escape(s.toISOExtString()) ~ "' as datetime)"; 228 } 229 230 string error() { 231 return fromCstring(mysql_error(mysql)); 232 } 233 234 void close() { 235 if(mysql) { 236 mysql_close(mysql); 237 mysql = null; 238 } 239 } 240 241 ~this() { 242 close(); 243 } 244 245 /// 246 int lastInsertId() { 247 return cast(int) mysql_insert_id(mysql); 248 } 249 250 251 252 /// Builds and executes an INERT INTO statement 253 int insert(string table, MySqlResult result, string[string] columnsToModify, string[] columnsToSkip) { 254 assert(!result.empty); 255 string sql = "INSERT INTO `" ~ table ~ "` "; 256 257 string cols = "("; 258 string vals = "("; 259 bool outputted = false; 260 261 string[string] columns; 262 auto cnames = result.fieldNames; 263 foreach(i, col; result.front.toStringArray) { 264 bool skipMe = false; 265 foreach(skip; columnsToSkip) { 266 if(cnames[i] == skip) { 267 skipMe = true; 268 break; 269 } 270 } 271 if(skipMe) 272 continue; 273 274 if(outputted) { 275 cols ~= ","; 276 vals ~= ","; 277 } else 278 outputted = true; 279 280 cols ~= cnames[i]; 281 282 if(result.columnIsNull[i] && cnames[i] !in columnsToModify) 283 vals ~= "NULL"; 284 else { 285 string v = col; 286 if(cnames[i] in columnsToModify) 287 v = columnsToModify[cnames[i]]; 288 289 vals ~= "'" ~ escape(v) ~ "'"; 290 291 } 292 } 293 294 cols ~= ")"; 295 vals ~= ")"; 296 297 sql ~= cols ~ " VALUES " ~ vals; 298 299 query(sql); 300 301 result.popFront; 302 303 return lastInsertId; 304 } 305 306 string escape(string str) { 307 ubyte[] buffer = new ubyte[str.length * 2 + 1]; 308 buffer.length = mysql_real_escape_string(mysql, buffer.ptr, cast(cstring) str.ptr, cast(uint) str.length); 309 310 return cast(string) buffer; 311 } 312 313 string escapeBinaryString(const(ubyte)[] data) { 314 return tohexsql(data); 315 } 316 317 string escaped(T...)(string sql, T t) { 318 static if(t.length > 0) { 319 string fixedup; 320 int pos = 0; 321 322 323 void escAndAdd(string str, int q) { 324 ubyte[] buffer = new ubyte[str.length * 2 + 1]; 325 buffer.length = mysql_real_escape_string(mysql, buffer.ptr, cast(cstring) str.ptr, str.length); 326 327 fixedup ~= sql[pos..q] ~ '\'' ~ cast(string) buffer ~ '\''; 328 329 } 330 331 foreach(a; t) { 332 int q = sql[pos..$].indexOf("?"); 333 if(q == -1) 334 break; 335 q += pos; 336 337 static if(__traits(compiles, t is null)) { 338 if(t is null) 339 fixedup ~= sql[pos..q] ~ "NULL"; 340 else 341 escAndAdd(to!string(*a), q); 342 } else { 343 string str = to!string(a); 344 escAndAdd(str, q); 345 } 346 347 pos = q+1; 348 } 349 350 fixedup ~= sql[pos..$]; 351 352 sql = fixedup; 353 354 //writefln("\n\nExecuting sql: %s", sql); 355 } 356 357 return sql; 358 } 359 360 361 /// Gets a minimal ORM object from a query 362 ResultByDataObject!R queryDataObject(R = DataObject, T...)(string sql, T t) { 363 // modify sql for the best data object grabbing 364 sql = fixupSqlForDataObjectUse(sql); 365 366 auto magic = query(sql, t); 367 return ResultByDataObject!R(cast(MySqlResult) magic, this); 368 } 369 370 371 /// ditto 372 ResultByDataObject!R queryDataObjectWithCustomKeys(R = DataObject, T...)(string[string] keyMapping, string sql, T t) { 373 sql = fixupSqlForDataObjectUse(sql, keyMapping); 374 375 auto magic = query(sql, t); 376 return ResultByDataObject!R(cast(MySqlResult) magic, this); 377 } 378 379 380 381 /// 382 int affectedRows() { 383 return cast(int) mysql_affected_rows(mysql); 384 } 385 386 override ResultSet queryImpl(string sql, Variant[] args...) { 387 sql = escapedVariants(this, sql, args); 388 389 enforce!(SqlException)( 390 !mysql_query(mysql, toCstring(sql)), 391 error() ~ " :::: " ~ sql); 392 393 return new MySqlResult(mysql_store_result(mysql), sql); 394 } 395 /+ 396 Result queryOld(T...)(string sql, T t) { 397 sql = escaped(sql, t); 398 399 if(sql.length == 0) 400 throw new DatabaseException("empty query"); 401 /* 402 static int queryCount = 0; 403 queryCount++; 404 if(sql.indexOf("INSERT") != -1) 405 stderr.writefln("%d: %s", queryCount, sql.replace("\n", " ").replace("\t", "")); 406 */ 407 408 version(dryRun) { 409 pragma(msg, "This is a dry run compile, no queries will be run"); 410 writeln(sql); 411 return Result(null, null); 412 } 413 414 enforceEx!(DatabaseException)( 415 !mysql_query(mysql, toCstring(sql)), 416 error() ~ " :::: " ~ sql); 417 418 return Result(mysql_store_result(mysql), sql); 419 } 420 +/ 421 /+ 422 struct ResultByAssoc { 423 this(Result* r) { 424 result = r; 425 fields = r.fieldNames(); 426 } 427 428 ulong length() { return result.length; } 429 bool empty() { return result.empty; } 430 void popFront() { result.popFront(); } 431 string[string] front() { 432 auto r = result.front; 433 string[string] ret; 434 foreach(i, a; r) { 435 ret[fields[i]] = a; 436 } 437 438 return ret; 439 } 440 441 @disable this(this) { } 442 443 string[] fields; 444 Result* result; 445 } 446 447 448 struct ResultByStruct(T) { 449 this(Result* r) { 450 result = r; 451 fields = r.fieldNames(); 452 } 453 454 ulong length() { return result.length; } 455 bool empty() { return result.empty; } 456 void popFront() { result.popFront(); } 457 T front() { 458 auto r = result.front; 459 string[string] ret; 460 foreach(i, a; r) { 461 ret[fields[i]] = a; 462 } 463 464 T s; 465 // FIXME: should use tupleOf 466 foreach(member; s.tupleof) { 467 if(member.stringof in ret) 468 member = to!(typeof(member))(ret[member]); 469 } 470 471 return s; 472 } 473 474 @disable this(this) { } 475 476 string[] fields; 477 Result* result; 478 } 479 +/ 480 481 /+ 482 483 484 struct Result { 485 private Result* heaped() { 486 auto r = new Result(result, sql, false); 487 488 r.tupleof = this.tupleof; 489 490 this.itemsTotal = 0; 491 this.result = null; 492 493 return r; 494 } 495 496 this(MYSQL_RES* r, string sql, bool prime = true) { 497 result = r; 498 itemsTotal = length; 499 itemsUsed = 0; 500 this.sql = sql; 501 // prime it here 502 if(prime && itemsTotal) 503 fetchNext(); 504 } 505 506 string sql; 507 508 ~this() { 509 if(result !is null) 510 mysql_free_result(result); 511 } 512 513 /+ 514 string[string][] fetchAssoc() { 515 516 } 517 +/ 518 519 ResultByAssoc byAssoc() { 520 return ResultByAssoc(&this); 521 } 522 523 ResultByStruct!(T) byStruct(T)() { 524 return ResultByStruct!(T)(&this); 525 } 526 527 string[] fieldNames() { 528 int numFields = mysql_num_fields(result); 529 auto fields = mysql_fetch_fields(result); 530 531 string[] names; 532 for(int i = 0; i < numFields; i++) { 533 names ~= fromCstring(fields[i].name); 534 } 535 536 return names; 537 } 538 539 MYSQL_FIELD[] fields() { 540 int numFields = mysql_num_fields(result); 541 auto fields = mysql_fetch_fields(result); 542 543 MYSQL_FIELD[] ret; 544 for(int i = 0; i < numFields; i++) { 545 ret ~= fields[i]; 546 } 547 548 return ret; 549 } 550 551 ulong length() { 552 if(result is null) 553 return 0; 554 return mysql_num_rows(result); 555 } 556 557 bool empty() { 558 return itemsUsed == itemsTotal; 559 } 560 561 Row front() { 562 return row; 563 } 564 565 void popFront() { 566 itemsUsed++; 567 if(itemsUsed < itemsTotal) { 568 fetchNext(); 569 } 570 } 571 572 void fetchNext() { 573 auto r = mysql_fetch_row(result); 574 uint numFields = mysql_num_fields(result); 575 uint* lengths = mysql_fetch_lengths(result); 576 row.length = 0; 577 // potential FIXME: not really binary safe 578 579 columnIsNull.length = numFields; 580 for(int a = 0; a < numFields; a++) { 581 if(*(r+a) is null) { 582 row ~= null; 583 columnIsNull[a] = true; 584 } else { 585 row ~= fromCstring(*(r+a), *(lengths + a)); 586 columnIsNull[a] = false; 587 } 588 } 589 } 590 591 @disable this(this) {} 592 private MYSQL_RES* result; 593 594 ulong itemsTotal; 595 ulong itemsUsed; 596 597 alias string[] Row; 598 599 Row row; 600 bool[] columnIsNull; // FIXME: should be part of the row 601 } 602 +/ 603 MYSQL* getHandle() { 604 return mysql; 605 } 606 607 private: 608 MYSQL* mysql; 609 } 610 611 struct ResultByDataObject(ObjType) if (is(ObjType : DataObject)) { 612 this(MySqlResult r, MySql mysql) { 613 result = r; 614 auto fields = r.fields(); 615 this.mysql = mysql; 616 617 foreach(i, f; fields) { 618 string tbl = fromCstring(f.org_table is null ? f.table : f.org_table, f.org_table is null ? f.table_length : f.org_table_length); 619 mappings[fromCstring(f.name)] = tuple( 620 tbl, 621 fromCstring(f.org_name is null ? f.name : f.org_name, f.org_name is null ? f.name_length : f.org_name_length)); 622 } 623 624 625 } 626 627 Tuple!(string, string)[string] mappings; 628 629 ulong length() { return result.length; } 630 bool empty() { return result.empty; } 631 void popFront() { result.popFront(); } 632 ObjType front() { 633 return new ObjType(mysql, result.front.toAA, mappings); 634 } 635 // would it be good to add a new() method? would be valid even if empty 636 // it'd just fill in the ID's at random and allow you to do the rest 637 638 @disable this(this) { } 639 640 MySqlResult result; 641 MySql mysql; 642 } 643 644 645 // thanks to 0xEAB on discord for sending me initial prepared statement support 646 647 struct Statement 648 { 649 ~this() 650 { 651 if (this.statement !is null) 652 { 653 this.statement.mysql_stmt_close(); 654 this.statement = null; 655 } 656 } 657 658 void reset() 659 { 660 mysql_stmt_reset(statement); 661 } 662 663 private: 664 MYSQL_STMT* statement; 665 MYSQL_BIND[] params; 666 } 667 668 Statement* prepare(MySql m, string query) @trusted 669 { 670 MYSQL_STMT* s = m.getHandle.mysql_stmt_init(); 671 immutable x = s.mysql_stmt_prepare(query.toStringz, cast(int) query.length); 672 673 if (x != 0) 674 { 675 throw new Exception(m.getHandle.mysql_error.fromCstring); 676 } 677 678 return new Statement(s); 679 } 680 681 import std.traits : isNumeric; 682 683 void bindParameter(T)(Statement* s, ref T value) if (isNumeric!T) 684 { 685 import std.traits : isUnsigned; 686 687 MYSQL_BIND p = MYSQL_BIND(); 688 689 p.buffer = &value; 690 p.buffer_type = mySqlType!T; 691 p.is_unsigned = isUnsigned!T; 692 693 s.params ~= p; 694 immutable x = s.statement.mysql_stmt_bind_param(&(s.params[$ - 1])); 695 696 if (x != 0) 697 { 698 throw new Exception(s.statement.mysql_stmt_error.fromStringz.to!string); 699 } 700 } 701 702 void bindParameterNull(Statement* s) 703 { 704 MYSQL_BIND p = MYSQL_BIND(); 705 706 p.buffer_type = enum_field_types.MYSQL_TYPE_NULL; 707 708 s.params ~= p; 709 immutable x = s.statement.mysql_stmt_bind_param(null); 710 711 if (x != 0) 712 { 713 throw new Exception(s.statement.mysql_stmt_error.fromStringz.to!string); 714 } 715 } 716 717 void bindParameter(T)(Statement* s, T value) if (is(T == string)) 718 { 719 import std.traits : isUnsigned; 720 721 MYSQL_BIND p = MYSQL_BIND(); 722 723 p.buffer = cast(void*) value.toCstring(); 724 p.buffer_type = mySqlType!string; 725 p.buffer_length = value.length; 726 727 s.params ~= p; 728 immutable x = s.statement.mysql_stmt_bind_param(&s.params[$ - 1]); 729 730 if (x != 0) 731 { 732 throw new Exception(s.statement.mysql_stmt_error.fromStringz.to!string); 733 } 734 } 735 736 void execute(Statement* s) @trusted 737 { 738 immutable x = s.statement.mysql_stmt_execute(); 739 740 if (x != 0) 741 { 742 throw new Exception(s.statement.mysql_stmt_error.fromStringz.to!string); 743 } 744 } 745 746 747 extern(System) { 748 /* 749 from <my_alloc.h> 750 original header actually contains members, 751 but guess we don't need them here 752 */ 753 struct USED_MEM; 754 755 /* 756 from <my_alloc.h> 757 */ 758 struct MEM_ROOT 759 { 760 USED_MEM* free; /* blocks with free memory in it */ 761 USED_MEM* used; /* blocks almost without free memory */ 762 USED_MEM* pre_alloc; /* preallocated block */ 763 /* if block have less memory it will be put in 'used' list */ 764 size_t min_malloc; 765 size_t block_size; /* initial block size */ 766 uint block_num; /* allocated blocks counter */ 767 /* 768 first free block in queue test counter (if it exceed 769 MAX_BLOCK_USAGE_BEFORE_DROP block will be dropped in 'used' list) 770 */ 771 uint first_block_usage; 772 773 void function () error_handler; 774 } 775 776 /* 777 from <mysql_com.h> 778 779 original header actually contains members, 780 but guess we don't need them here 781 */ 782 struct NET; 783 784 /* from <mysql_com.h> */ 785 enum MYSQL_ERRMSG_SIZE = 512; 786 787 /* from <mysql_com.h> */ 788 enum enum_field_types { 789 MYSQL_TYPE_DECIMAL, MYSQL_TYPE_TINY, 790 MYSQL_TYPE_SHORT, MYSQL_TYPE_LONG, 791 MYSQL_TYPE_FLOAT, MYSQL_TYPE_DOUBLE, 792 MYSQL_TYPE_NULL, MYSQL_TYPE_TIMESTAMP, 793 MYSQL_TYPE_LONGLONG,MYSQL_TYPE_INT24, 794 MYSQL_TYPE_DATE, MYSQL_TYPE_TIME, 795 MYSQL_TYPE_DATETIME, MYSQL_TYPE_YEAR, 796 MYSQL_TYPE_NEWDATE, MYSQL_TYPE_VARCHAR, 797 MYSQL_TYPE_BIT, 798 799 /* 800 mysql-5.6 compatibility temporal types. 801 They're only used internally for reading RBR 802 mysql-5.6 binary log events and mysql-5.6 frm files. 803 They're never sent to the client. 804 */ 805 MYSQL_TYPE_TIMESTAMP2, 806 MYSQL_TYPE_DATETIME2, 807 MYSQL_TYPE_TIME2, 808 809 MYSQL_TYPE_NEWDECIMAL=246, 810 811 MYSQL_TYPE_ENUM=247, 812 MYSQL_TYPE_SET=248, 813 MYSQL_TYPE_TINY_BLOB=249, 814 MYSQL_TYPE_MEDIUM_BLOB=250, 815 MYSQL_TYPE_LONG_BLOB=251, 816 MYSQL_TYPE_BLOB=252, 817 MYSQL_TYPE_VAR_STRING=253, 818 MYSQL_TYPE_STRING=254, 819 MYSQL_TYPE_GEOMETRY=255 820 } 821 822 /* from <my_list.h>*/ 823 struct LIST 824 { 825 LIST* prev; 826 LIST* next; 827 void* data; 828 } 829 830 struct MYSQL; 831 struct MYSQL_RES; 832 /* typedef */ alias const(ubyte)* cstring; 833 834 alias my_bool = char; 835 alias my_ulonglong = ulong; 836 837 struct MYSQL_FIELD { 838 cstring name; /* Name of column */ 839 cstring org_name; /* Original column name, if an alias */ 840 cstring table; /* Table of column if column was a field */ 841 cstring org_table; /* Org table name, if table was an alias */ 842 cstring db; /* Database for table */ 843 cstring catalog; /* Catalog for table */ 844 cstring def; /* Default value (set by mysql_list_fields) */ 845 c_ulong length; /* Width of column (create length) */ 846 c_ulong max_length; /* Max width for selected set */ 847 uint name_length; 848 uint org_name_length; 849 uint table_length; 850 uint org_table_length; 851 uint db_length; 852 uint catalog_length; 853 uint def_length; 854 uint flags; /* Div flags */ 855 uint decimals; /* Number of decimals in field */ 856 uint charsetnr; /* Character set */ 857 uint type; /* Type of field. See mysql_com.h for types */ 858 // type is actually an enum btw 859 860 version(MySQL_51) { 861 void* extension; 862 } 863 } 864 865 struct MYSQL_ROWS 866 { 867 MYSQL_ROWS* next; /* list of rows */ 868 MYSQL_ROW data; 869 c_ulong length; 870 } 871 872 alias MYSQL_ROW_OFFSET = MYSQL_ROWS*; /* offset to current row */ 873 874 struct EMBEDDED_QUERY_RESULT; 875 876 struct MYSQL_DATA 877 { 878 MYSQL_ROWS* data; 879 EMBEDDED_QUERY_RESULT* embedded_info; 880 MEM_ROOT alloc; 881 my_ulonglong rows; 882 uint fields; 883 884 version(MySQL_51) { 885 /* extra info for embedded library */ 886 void* extension; 887 } 888 } 889 890 /* statement state */ 891 enum enum_mysql_stmt_state 892 { 893 MYSQL_STMT_INIT_DONE = 1, 894 MYSQL_STMT_PREPARE_DONE = 2, 895 MYSQL_STMT_EXECUTE_DONE = 3, 896 MYSQL_STMT_FETCH_DONE = 4 897 } 898 899 enum enum_stmt_attr_type 900 { 901 /** 902 When doing mysql_stmt_store_result calculate max_length attribute 903 of statement metadata. This is to be consistent with the old API, 904 where this was done automatically. 905 In the new API we do that only by request because it slows down 906 mysql_stmt_store_result sufficiently. 907 */ 908 STMT_ATTR_UPDATE_MAX_LENGTH = 0, 909 /** 910 unsigned long with combination of cursor flags (read only, for update, etc) 911 */ 912 STMT_ATTR_CURSOR_TYPE = 1, 913 /** 914 Amount of rows to retrieve from server per one fetch if using cursors. 915 Accepts unsigned long attribute in the range 1 - ulong_max 916 */ 917 STMT_ATTR_PREFETCH_ROWS = 2 918 } 919 920 struct MYSQL_BIND 921 { 922 c_ulong* length; /* output length pointer */ 923 my_bool* is_null; /* Pointer to null indicator */ 924 void* buffer; /* buffer to get/put data */ 925 /* set this if you want to track data truncations happened during fetch */ 926 my_bool* error; 927 ubyte* row_ptr; /* for the current data position */ 928 void function (NET* net, MYSQL_BIND* param) store_param_func; 929 void function (MYSQL_BIND*, MYSQL_FIELD*, ubyte** row) fetch_result; 930 void function (MYSQL_BIND*, MYSQL_FIELD*, ubyte** row) skip_result; 931 /* output buffer length, must be set when fetching str/binary */ 932 c_ulong buffer_length; 933 c_ulong offset; /* offset position for char/binary fetch */ 934 c_ulong length_value; /* Used if length is 0 */ 935 uint param_number; /* For null count and error messages */ 936 uint pack_length; /* Internal length for packed data */ 937 enum_field_types buffer_type; /* buffer type */ 938 my_bool error_value; /* used if error is 0 */ 939 my_bool is_unsigned; /* set if integer type is unsigned */ 940 my_bool long_data_used; /* If used with mysql_send_long_data */ 941 my_bool is_null_value; /* Used if is_null is 0 */ 942 void* extension; 943 } 944 945 struct st_mysql_stmt_extension; 946 947 /* statement handler */ 948 struct MYSQL_STMT 949 { 950 MEM_ROOT mem_root; /* root allocations */ 951 LIST list; /* list to keep track of all stmts */ 952 MYSQL* mysql; /* connection handle */ 953 MYSQL_BIND* params; /* input parameters */ 954 MYSQL_BIND* bind; /* output parameters */ 955 MYSQL_FIELD* fields; /* result set metadata */ 956 MYSQL_DATA result; /* cached result set */ 957 MYSQL_ROWS* data_cursor; /* current row in cached result */ 958 /* 959 mysql_stmt_fetch() calls this function to fetch one row (it's different 960 for buffered, unbuffered and cursor fetch). 961 */ 962 int function (MYSQL_STMT* stmt, ubyte** row) read_row_func; 963 /* copy of mysql->affected_rows after statement execution */ 964 my_ulonglong affected_rows; 965 my_ulonglong insert_id; /* copy of mysql->insert_id */ 966 c_ulong stmt_id; /* Id for prepared statement */ 967 c_ulong flags; /* i.e. type of cursor to open */ 968 c_ulong prefetch_rows; /* number of rows per one COM_FETCH */ 969 /* 970 Copied from mysql->server_status after execute/fetch to know 971 server-side cursor status for this statement. 972 */ 973 uint server_status; 974 uint last_errno; /* error code */ 975 uint param_count; /* input parameter count */ 976 uint field_count; /* number of columns in result set */ 977 enum_mysql_stmt_state state; /* statement state */ 978 char[MYSQL_ERRMSG_SIZE] last_error; /* error message */ 979 char[6] sqlstate; 980 /* Types of input parameters should be sent to server */ 981 my_bool send_types_to_server; 982 my_bool bind_param_done; /* input buffers were supplied */ 983 ubyte bind_result_done; /* output buffers were supplied */ 984 /* mysql_stmt_close() had to cancel this result */ 985 my_bool unbuffered_fetch_cancelled; 986 /* 987 Is set to true if we need to calculate field->max_length for 988 metadata fields when doing mysql_stmt_store_result. 989 */ 990 my_bool update_max_length; 991 st_mysql_stmt_extension* extension; 992 } 993 994 /* typedef */ alias cstring* MYSQL_ROW; 995 996 cstring mysql_get_client_info(); 997 MYSQL* mysql_init(MYSQL*); 998 uint mysql_errno(MYSQL*); 999 cstring mysql_error(MYSQL*); 1000 1001 MYSQL* mysql_real_connect(MYSQL*, cstring, cstring, cstring, cstring, uint, cstring, c_ulong); 1002 1003 int mysql_query(MYSQL*, cstring); 1004 1005 void mysql_close(MYSQL*); 1006 1007 ulong mysql_num_rows(MYSQL_RES*); 1008 uint mysql_num_fields(MYSQL_RES*); 1009 bool mysql_eof(MYSQL_RES*); 1010 1011 ulong mysql_affected_rows(MYSQL*); 1012 ulong mysql_insert_id(MYSQL*); 1013 1014 MYSQL_RES* mysql_store_result(MYSQL*); 1015 MYSQL_RES* mysql_use_result(MYSQL*); 1016 1017 MYSQL_ROW mysql_fetch_row(MYSQL_RES *); 1018 c_ulong* mysql_fetch_lengths(MYSQL_RES*); 1019 MYSQL_FIELD* mysql_fetch_field(MYSQL_RES*); 1020 MYSQL_FIELD* mysql_fetch_fields(MYSQL_RES*); 1021 1022 uint mysql_real_escape_string(MYSQL*, ubyte* to, cstring from, c_ulong length); 1023 1024 void mysql_free_result(MYSQL_RES*); 1025 1026 MYSQL_STMT* mysql_stmt_init (MYSQL* mysql); 1027 int mysql_stmt_prepare (MYSQL_STMT* stmt, const(char)* query, c_ulong length); 1028 int mysql_stmt_execute (MYSQL_STMT* stmt); 1029 my_bool mysql_stmt_bind_param (MYSQL_STMT* stmt, MYSQL_BIND* bnd); 1030 my_bool mysql_stmt_close (MYSQL_STMT* stmt); 1031 my_bool mysql_stmt_free_result (MYSQL_STMT* stmt); 1032 my_bool mysql_stmt_reset (MYSQL_STMT* stmt); 1033 uint mysql_stmt_errno (MYSQL_STMT* stmt); 1034 const(char)* mysql_stmt_error (MYSQL_STMT* stmt); 1035 const(char)* mysql_stmt_sqlstate (MYSQL_STMT* stmt); 1036 my_ulonglong mysql_stmt_num_rows (MYSQL_STMT* stmt); 1037 my_ulonglong mysql_stmt_affected_rows (MYSQL_STMT* stmt); 1038 my_ulonglong mysql_stmt_insert_id (MYSQL_STMT* stmt); 1039 1040 } 1041 1042 import std.string; 1043 cstring toCstring(string c) { 1044 return cast(cstring) toStringz(c); 1045 } 1046 1047 import std.array; 1048 string fromCstring(cstring c, size_t len = size_t.max) @system { 1049 string ret; 1050 if(c is null) 1051 return null; 1052 if(len == 0) 1053 return ""; 1054 if(len == size_t.max) { 1055 auto iterator = c; 1056 len = 0; 1057 while(*iterator) { 1058 iterator++; 1059 len++; 1060 } 1061 assert(len >= 0); 1062 } 1063 1064 ret = cast(string) (c[0 .. len].idup); 1065 1066 return ret; 1067 } 1068 1069 enum_field_types getMySqlType(T)() { 1070 static if (is(T == bool)) 1071 return enum_field_types.MYSQL_TYPE_TINY; 1072 1073 static if (is(T == char)) 1074 return enum_field_types.MYSQL_TYPE_TINY; 1075 1076 static if (is(T == byte) || is(T == ubyte)) 1077 return enum_field_types.MYSQL_TYPE_TINY; 1078 1079 else static if (is(T == short) || is(T == ushort)) 1080 return enum_field_types.MYSQL_TYPE_SHORT; 1081 1082 else static if (is(T == int) || is(T == uint)) 1083 return enum_field_types.MYSQL_TYPE_LONG; 1084 1085 else static if (is(T == long) || is(T == ulong)) 1086 return enum_field_types.MYSQL_TYPE_LONGLONG; 1087 1088 else static if (is(T == string)) 1089 return enum_field_types.MYSQL_TYPE_STRING; 1090 1091 else static if (is(T == float)) 1092 return enum_field_types.MYSQL_TYPE_FLOAT; 1093 1094 else static if (is(T == double)) 1095 return enum_field_types.MYSQL_TYPE_DOUBLE; 1096 1097 //else static if (is(T == byte[])) 1098 // return enum_field_types.MYSQL_TYPE_BLOB; 1099 1100 else 1101 static assert("No MySQL equivalent known for " ~ T); 1102 } 1103 1104 enum enum_field_types mySqlType(T) = getMySqlType!T; 1105 1106 // FIXME: this should work generically with all database types and them moved to database.d 1107 /// 1108 Ret queryOneRow(Ret = Row, DB, string file = __FILE__, size_t line = __LINE__, T...)(DB db, string sql, T t) if( 1109 (is(DB : Database)) 1110 // && (is(Ret == Row) || is(Ret : DataObject))) 1111 ) 1112 { 1113 static if(is(Ret : DataObject) && is(DB == MySql)) { 1114 auto res = db.queryDataObject!Ret(sql, t); 1115 if(res.empty) 1116 throw new EmptyResultException("result was empty", file, line); 1117 return res.front; 1118 } else static if(is(Ret == Row)) { 1119 auto res = db.query(sql, t); 1120 if(res.empty) 1121 throw new EmptyResultException("result was empty", file, line); 1122 return res.front; 1123 } else static assert(0, "Unsupported single row query return value, " ~ Ret.stringof); 1124 } 1125 1126 /// 1127 class EmptyResultException : Exception { 1128 this(string message, string file = __FILE__, size_t line = __LINE__) { 1129 super(message, file, line); 1130 } 1131 } 1132 1133 1134 /* 1135 void main() { 1136 auto mysql = new MySql("localhost", "uname", "password", "test"); 1137 scope(exit) delete mysql; 1138 1139 mysql.query("INSERT INTO users (id, password) VALUES (?, ?)", 10, "lol"); 1140 1141 foreach(row; mysql.query("SELECT * FROM users")) { 1142 writefln("%s %s %s %s", row["id"], row[0], row[1], row["username"]); 1143 } 1144 } 1145 */ 1146 1147 /* 1148 struct ResultByStruct(T) { 1149 this(MySql.Result* r) { 1150 result = r; 1151 fields = r.fieldNames(); 1152 } 1153 1154 ulong length() { return result.length; } 1155 bool empty() { return result.empty; } 1156 void popFront() { result.popFront(); } 1157 T front() { 1158 auto r = result.front; 1159 T ret; 1160 foreach(i, a; r) { 1161 ret[fields[i]] = a; 1162 } 1163 1164 return ret; 1165 } 1166 1167 @disable this(this) { } 1168 1169 string[] fields; 1170 MySql.Result* result; 1171 } 1172 */ 1173 1174 1175 /+ 1176 mysql.linq.tablename.field[key] // select field from tablename where id = key 1177 1178 mysql.link["name"].table.field[key] // select field from table where name = key 1179 1180 1181 auto q = mysql.prepQuery("select id from table where something"); 1182 q.sort("name"); 1183 q.limit(start, count); 1184 q.page(3, pagelength = ?); 1185 1186 q.execute(params here); // returns the same Result range as query 1187 +/ 1188 1189 /* 1190 void main() { 1191 auto db = new MySql("localhost", "uname", "password", "test"); 1192 foreach(item; db.queryDataObject("SELECT users.*, username 1193 FROM users, password_manager_accounts 1194 WHERE password_manager_accounts.user_id = users.id LIMIT 5")) { 1195 writefln("item: %s, %s", item.id, item.username); 1196 item.first = "new"; 1197 item.last = "new2"; 1198 item.username = "kill"; 1199 //item.commitChanges(); 1200 } 1201 } 1202 */ 1203 1204 1205 /* 1206 Copyright: Adam D. Ruppe, 2009 - 2024 1207 License: GPL (erroneously started to be Boost in 2011) 1208 Authors: Adam D. Ruppe, with contributions from Nick Sabalausky 1209 */ 1210