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