├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── dub.json └── src └── mysql ├── appender.d ├── client.d ├── connection.d ├── exception.d ├── inserter.d ├── package.d ├── packet.d ├── protocol.d ├── row.d ├── socket.d ├── ssl.d └── type.d /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.{c,h,d,di,dd,json}] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_style = tab 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | charset = utf-8 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.a 2 | .dub 3 | dub.selections.json 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 eBookingServices 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mysql-lited 2 | A lightweight native MySQL/MariaDB driver written in D 3 | 4 | The goal is a native driver that re-uses the same buffers and the stack as much as possible, 5 | avoiding unnecessary allocations and work for the garbage collector 6 | 7 | 8 | ## notes 9 | - supports all MySQL types with conversion from/to D native types 10 | - results can be retrieved through a flexible and efficient callback interface 11 | - socket type is a template parameter - currently only a vibesocket is implemented 12 | - both the text and the binary protocol are supported 13 | 14 | 15 | ## example 16 | ```d 17 | import std.stdio; 18 | 19 | import mysql; 20 | 21 | 22 | void usedb() { 23 | 24 | // use the default mysql client - uses only prepared statements 25 | auto client = new MySQLClient("host=sql.moo.com;user=root;pwd=god;db=mew"); 26 | auto conn = client.lockConnection(); 27 | 28 | 29 | // use the text protocol instead - instantiate the MySQLClientT template with appropriate arguments 30 | alias MySQLTextClient = MySQLClientT!(VibeSocket, ConnectionOptions.TextProtocol | ConnectionOptions.TextProtocolCheckNoArgs); 31 | auto textClient = new MySQLTextClient("host=sql.moo.com;user=root;pwd=god;db=mew"); 32 | auto textConn = textClient.lockConnection(); 33 | 34 | 35 | // change database 36 | conn.use("mewmew"); 37 | 38 | 39 | // simple insert statement 40 | conn.execute("insert into users (name, email) values (?, ?)", "frank", "thetank@cowabanga.com"); 41 | auto id = conn.insertID; 42 | 43 | struct User { 44 | string name; 45 | string email; 46 | } 47 | 48 | // simple select statement 49 | User[] users; 50 | conn.execute("select name, email from users where id > ?", 13, (MySQLRow row) { 51 | users ~= row.toStruct!User; 52 | }); 53 | 54 | 55 | // batch inserter - inserts in packets of 128k bytes 56 | auto insert = inserter(conn, "users_copy", "name", "email"); 57 | foreach(user; users) 58 | insert.row(user.name, user.email); 59 | insert.flush; 60 | 61 | //struct inserter - insterts struct directly using the field names and UDAs. 62 | struct Info{ 63 | string employee = "employee"; 64 | int duration_in_months = 12; 65 | } 66 | 67 | struct InsuranceInfo { 68 | int number = 50; 69 | Date started = Date(2015,12,25); 70 | @ignore string description = "insurance description"; 71 | Info info; 72 | } 73 | 74 | struct BankInfo { 75 | string iban; 76 | string name; 77 | @as("country") string bankCountry; 78 | } 79 | 80 | struct Client { 81 | @as("name") string clientName = "default name"; 82 | @as("email") string emailAddress = "default email"; 83 | @as("token") string uniuqeToken = "default token"; 84 | @as("birth_date") Date birthDate = Date(1991, 9, 9); 85 | @ignore string moreInfoString; 86 | InsuranceInfo insurance; 87 | BankInfo bank; 88 | } 89 | 90 | Client client; 91 | auto inserter = inserter(conn, "client", "name", "email", "birth_date", "token", "bank.country", "bank.iban", "bank.name" , 92 | "insurance.number", "insurance.started", "insurance.info.employee", "insurance.info.duration_in_months"); 93 | inserter.row(client); 94 | 95 | auto dbClient = conn.fetchOne!Client("select * from client limit 1"); 96 | 97 | assert(client.serialize == dbClient.serialize) 98 | 99 | //batch insert struct array 100 | Client[] clients = [Client(), Client(), Client()]; 101 | insert.rows(clients); 102 | 103 | // re-usable prepared statements 104 | auto upd = conn.prepare("update users set sequence = ?, login_at = ?, secret = ? where id = ?"); 105 | ubyte[] bytes = [0x4D, 0x49, 0x4C, 0x4B]; 106 | foreach(i; 0..100) 107 | conn.execute(upd, i, Clock.currTime, MySQLBinary(bytes), i); 108 | 109 | 110 | // passing variable or large number of arguments 111 | string[] names; 112 | string[] emails; 113 | int[] ids = [1, 1, 3, 5, 8, 13]; 114 | conn.execute("select name from users where id in " ~ ids.placeholders, ids, (MySQLRow row) { 115 | writeln(row.name.peek!(char[])); // peek() avoids allocation - cannot use result outside delegate 116 | names ~= row.name.get!string; // get() duplicates - safe to use result outside delegate 117 | emails ~= row.email.get!string; 118 | }); 119 | 120 | 121 | // another query example 122 | conn.execute("select id, name, email from users where id > ?", 13, (size_t index /*optional*/, MySQLHeader header /*optional*/, MySQLRow row) { 123 | writeln(header[0].name, ": ", row.id.get!int); 124 | return (index < 5); // optionally return false to discard remaining results 125 | }); 126 | 127 | 128 | // structured row 129 | conn.execute("select name, email from users where length(name) > ?", 5, (MySQLRow row) { 130 | auto user = row.toStruct!User; // default is strict.yesIgnoreNull - a missing field in the row will throw 131 | // auto user = row.toStruct!(User, Strict.yes); // missing or null will throw 132 | // auto user = row.toStruct!(User, Strict.no); // missing or null will just be ignored 133 | writeln(user); 134 | }); 135 | 136 | 137 | // structured row with nested structs 138 | struct GeoRef { 139 | double lat; 140 | double lng; 141 | } 142 | 143 | struct Place { 144 | string name; 145 | GeoRef location; 146 | } 147 | 148 | conn.execute("select name, lat as `location.lat`, lng as `location.lng` from places", (MySQLRow row) { 149 | auto place = row.toStruct!Place; 150 | writeln(place.location); 151 | }); 152 | 153 | 154 | // structured row annotations 155 | struct PlaceFull { 156 | uint id; 157 | string name; 158 | @optional string thumbnail; // ok to be null or missing 159 | @optional GeoRef location; // nested fields ok to be null or missing 160 | @optional @as("contact_person") string contact; // optional, and sourced from field contact_person instead 161 | 162 | @ignore File tumbnail; // completely ignored 163 | } 164 | 165 | conn.execute("select id, name, thumbnail, lat as `location.lat`, lng as `location.lng`, contact_person from places", (MySQLRow row) { 166 | auto place = row.toStruct!PlaceFull; 167 | writeln(place.location); 168 | }); 169 | 170 | 171 | // automated struct member uncamelcase 172 | @uncamel struct PlaceOwner { 173 | uint placeID; // matches placeID and place_id 174 | uint locationId; // matches locationId and location_id 175 | string ownerFirstName; // matches ownerFirstName and owner_first_name 176 | string ownerLastName; // matches ownerLastName and owner_last_name 177 | string feedURL; // matches feedURL and feed_url 178 | } 179 | } 180 | ``` 181 | 182 | ## todo 183 | - add proper unit tests 184 | - implement COM\_STMT\_SEND\_LONG\_DATA, and a single parameter binding interface 185 | - make vibe-d dependency optional 186 | -------------------------------------------------------------------------------- /dub.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mysql-lited", 3 | "description": "Lightweight native MySQL/MariaDB driver", 4 | "copyright": "Copyright © 2015, Marcio Martins", 5 | "authors": [ "Márcio Martins" ], 6 | "license": "MIT License", 7 | "targetType": "library", 8 | "targetName": "mysql-lited", 9 | "dependencies": { 10 | "vibe-d": ">=0.7.22" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/mysql/appender.d: -------------------------------------------------------------------------------- 1 | module mysql.appender; 2 | 3 | 4 | import std.conv; 5 | import std.datetime; 6 | import std.format; 7 | import std.traits; 8 | import std.typecons; 9 | 10 | import mysql.protocol; 11 | import mysql.type; 12 | 13 | 14 | void appendValues(Appender, T)(ref Appender appender, T values) if (isArray!T && !isSomeString!(OriginalType!T)) { 15 | foreach (size_t i, value; values) { 16 | appendValue(appender, value); 17 | if (i != values.length-1) 18 | appender.put(','); 19 | } 20 | } 21 | 22 | void appendValue(Appender, T)(ref Appender appender, T value) if (is(Unqual!T == typeof(null))) { 23 | appender.put("null"); 24 | } 25 | 26 | void appendValue(Appender, T)(ref Appender appender, T value) if (isInstanceOf!(Nullable, T) || isInstanceOf!(NullableRef, T)) { 27 | if (value.isNull) { 28 | appendValue(appender, null); 29 | } else { 30 | appendValue(appender, value.get); 31 | } 32 | } 33 | 34 | void appendValue(Appender, T)(ref Appender appender, T value) if (isScalarType!T) { 35 | appender.put(cast(ubyte[])to!string(value)); 36 | } 37 | 38 | void appendValue(Appender, T)(ref Appender appender, T value) if (is(Unqual!T == SysTime)) { 39 | value = value.toUTC; 40 | 41 | auto hour = value.hour; 42 | auto minute = value.minute; 43 | auto second = value.second; 44 | auto usec = value.fracSecs.total!"usecs"; 45 | 46 | formattedWrite(appender, "%04d%02d%02d", value.year, value.month, value.day); 47 | if (hour | minute | second | usec) { 48 | formattedWrite(appender, "%02d%02d%02d", hour, minute, second); 49 | if (usec) 50 | formattedWrite(appender, ".%06d", usec); 51 | } 52 | } 53 | 54 | void appendValue(Appender, T)(ref Appender appender, T value) if (is(Unqual!T == DateTime)) { 55 | auto hour = value.hour; 56 | auto minute = value.minute; 57 | auto second = value.second; 58 | 59 | if (hour | minute | second) { 60 | formattedWrite(appender, "%04d%02d%02d%02d%02d%02d", value.year, value.month, value.day, hour, minute, second); 61 | } else { 62 | formattedWrite(appender, "%04d%02d%02d", value.year, value.month, value.day); 63 | } 64 | } 65 | 66 | void appendValue(Appender, T)(ref Appender appender, T value) if (is(Unqual!T == TimeOfDay)) { 67 | formattedWrite(appender, "%02d%02d%02d", value.hour, value.minute, value.second); 68 | } 69 | 70 | void appendValue(Appender, T)(ref Appender appender, T value) if (is(Unqual!T == Date)) { 71 | formattedWrite(appender, "%04d%02d%02d", value.year, value.month, value.day); 72 | } 73 | 74 | void appendValue(Appender, T)(ref Appender appender, T value) if (is(Unqual!T == Duration)) { 75 | auto parts = value.split(); 76 | if (parts.days) { 77 | appender.put('\''); 78 | formattedWrite(appender, "%d ", parts.days); 79 | } 80 | formattedWrite(appender, "%02d%02d%02d", parts.hours, parts.minutes, parts.seconds); 81 | if (parts.usecs) 82 | formattedWrite(appender, ".%06d ", parts.usecs); 83 | if (parts.days) 84 | appender.put('\''); 85 | } 86 | 87 | void appendValue(Appender, T)(ref Appender appender, T value) if (is(Unqual!T == MySQLFragment)) { 88 | appender.put(cast(char[])value.data); 89 | } 90 | 91 | void appendValue(Appender, T)(ref Appender appender, T value) if (is(Unqual!T == MySQLRawString)) { 92 | appender.put('\''); 93 | appender.put(cast(char[])value.data); 94 | appender.put('\''); 95 | } 96 | 97 | void appendValue(Appender, T)(ref Appender appender, T value) if (is(Unqual!T == MySQLBinary)) { 98 | appendValue(appender, value.data); 99 | } 100 | 101 | void appendValue(Appender, T)(ref Appender appender, T value) if (is(Unqual!T == MySQLValue)) { 102 | final switch(value.type) with (ColumnTypes) { 103 | case MYSQL_TYPE_NULL: 104 | appender.put("null"); 105 | break; 106 | case MYSQL_TYPE_TINY: 107 | if (value.isSigned) { 108 | appendValue(appender, value.peek!byte); 109 | } else { 110 | appendValue(appender, value.peek!ubyte); 111 | } 112 | break; 113 | case MYSQL_TYPE_YEAR: 114 | case MYSQL_TYPE_SHORT: 115 | if (value.isSigned) { 116 | appendValue(appender, value.peek!short); 117 | } else { 118 | appendValue(appender, value.peek!ushort); 119 | } 120 | break; 121 | case MYSQL_TYPE_INT24: 122 | case MYSQL_TYPE_LONG: 123 | if (value.isSigned) { 124 | appendValue(appender, value.peek!int); 125 | } else { 126 | appendValue(appender, value.peek!uint); 127 | } 128 | break; 129 | case MYSQL_TYPE_LONGLONG: 130 | if (value.isSigned) { 131 | appendValue(appender, value.peek!long); 132 | } else { 133 | appendValue(appender, value.peek!ulong); 134 | } 135 | break; 136 | case MYSQL_TYPE_DOUBLE: 137 | appendValue(appender, value.peek!double); 138 | break; 139 | case MYSQL_TYPE_FLOAT: 140 | appendValue(appender, value.peek!float); 141 | break; 142 | case MYSQL_TYPE_SET: 143 | case MYSQL_TYPE_ENUM: 144 | case MYSQL_TYPE_VARCHAR: 145 | case MYSQL_TYPE_VAR_STRING: 146 | case MYSQL_TYPE_STRING: 147 | case MYSQL_TYPE_JSON: 148 | case MYSQL_TYPE_NEWDECIMAL: 149 | case MYSQL_TYPE_DECIMAL: 150 | appendValue(appender, value.peek!(char[])); 151 | break; 152 | case MYSQL_TYPE_BIT: 153 | case MYSQL_TYPE_TINY_BLOB: 154 | case MYSQL_TYPE_MEDIUM_BLOB: 155 | case MYSQL_TYPE_LONG_BLOB: 156 | case MYSQL_TYPE_BLOB: 157 | case MYSQL_TYPE_GEOMETRY: 158 | appendValue(appender, value.peek!(ubyte[])); 159 | break; 160 | case MYSQL_TYPE_TIME: 161 | case MYSQL_TYPE_TIME2: 162 | appendValue(appender, value.peek!Duration); 163 | break; 164 | case MYSQL_TYPE_DATE: 165 | case MYSQL_TYPE_NEWDATE: 166 | case MYSQL_TYPE_DATETIME: 167 | case MYSQL_TYPE_DATETIME2: 168 | case MYSQL_TYPE_TIMESTAMP: 169 | case MYSQL_TYPE_TIMESTAMP2: 170 | appendValue(appender, value.peek!SysTime); 171 | break; 172 | } 173 | } 174 | 175 | void appendValue(Appender, T)(ref Appender appender, T value) if (isArray!T && (is(Unqual!(typeof(T.init[0])) == ubyte) || is(Unqual!(typeof(T.init[0])) == char))) { 176 | appender.put('\''); 177 | auto ptr = value.ptr; 178 | auto end = value.ptr + value.length; 179 | while (ptr != end) { 180 | switch(*ptr) { 181 | case '\\': 182 | case '\'': 183 | appender.put('\\'); 184 | goto default; 185 | default: 186 | appender.put(*ptr++); 187 | } 188 | } 189 | appender.put('\''); 190 | } 191 | -------------------------------------------------------------------------------- /src/mysql/client.d: -------------------------------------------------------------------------------- 1 | module mysql.client; 2 | 3 | import vibe.core.connectionpool; 4 | 5 | public import mysql.connection; 6 | import mysql.socket; 7 | 8 | 9 | final class MySQLClientT(SocketType, ConnectionOptions Options = ConnectionOptions.Default) { 10 | this(string connectionString) { 11 | connections_ = new ConnectionPoolType({ 12 | auto ret = new ConnectionType(); 13 | ret.connect(connectionString); 14 | return ret; 15 | }); 16 | } 17 | 18 | this(ConnectionSettings settings) { 19 | connections_ = new ConnectionPoolType({ 20 | auto ret = new ConnectionType(); 21 | ret.connect(settings); 22 | return ret; 23 | }); 24 | } 25 | 26 | this(string host, ushort port, string user, string pwd, string db) { 27 | connections_ = new ConnectionPoolType({ 28 | auto ret = new ConnectionType(); 29 | ret.connect(host, port, user, pwd, db); 30 | return ret; 31 | }); 32 | } 33 | 34 | auto lockConnection() { 35 | auto connection = connections_.lockConnection(); 36 | connection.reuse(); 37 | return connection; 38 | } 39 | 40 | @property const(ConnectionPoolType) pool() inout { 41 | return connections_; 42 | } 43 | 44 | alias LockedConnection = vibe.core.connectionpool.LockedConnection!(ConnectionType*); 45 | alias ConnectionType = Connection!(VibeSocket, Options); 46 | alias ConnectionPoolType = ConnectionPool!(ConnectionType*); 47 | 48 | private ConnectionPoolType connections_; 49 | } 50 | 51 | alias MySQLClient = MySQLClientT!VibeSocket; 52 | -------------------------------------------------------------------------------- /src/mysql/connection.d: -------------------------------------------------------------------------------- 1 | module mysql.connection; 2 | 3 | 4 | import std.algorithm; 5 | import std.array; 6 | import std.conv : to; 7 | import std.string; 8 | import std.traits; 9 | import std.uni : sicmp; 10 | import std.utf : decode, UseReplacementDchar; 11 | 12 | import mysql.appender; 13 | public import mysql.exception; 14 | import mysql.packet; 15 | import mysql.protocol; 16 | import mysql.ssl; 17 | public import mysql.type; 18 | 19 | 20 | immutable CapabilityFlags DefaultClientCaps = CapabilityFlags.CLIENT_LONG_PASSWORD | CapabilityFlags.CLIENT_LONG_FLAG | 21 | CapabilityFlags.CLIENT_CONNECT_WITH_DB | CapabilityFlags.CLIENT_PROTOCOL_41 | CapabilityFlags.CLIENT_SECURE_CONNECTION | CapabilityFlags.CLIENT_SESSION_TRACK; 22 | 23 | 24 | struct ConnectionStatus { 25 | ulong affected; 26 | ulong matched; 27 | ulong changed; 28 | ulong insertID; 29 | ushort flags; 30 | ushort error; 31 | ushort warnings; 32 | } 33 | 34 | 35 | struct ConnectionSettings { 36 | this(const(char)[] connectionString) { 37 | parse(connectionString); 38 | } 39 | 40 | void parse(const(char)[] connectionString) { 41 | auto remaining = connectionString; 42 | 43 | auto indexValue = remaining.indexOf("="); 44 | while (!remaining.empty) { 45 | auto indexValueEnd = remaining.indexOf(";", indexValue); 46 | if (indexValueEnd <= 0) 47 | indexValueEnd = remaining.length; 48 | 49 | auto name = strip(remaining[0..indexValue]); 50 | auto value = strip(remaining[indexValue+1..indexValueEnd]); 51 | 52 | switch (name) { 53 | case "host": 54 | host = value; 55 | break; 56 | case "user": 57 | user = value; 58 | break; 59 | case "pwd": 60 | pwd = value; 61 | break; 62 | case "db": 63 | db = value; 64 | break; 65 | case "port": 66 | port = to!ushort(value); 67 | break; 68 | case "ssl": 69 | switch (value) { 70 | case "0": 71 | case "no": 72 | case "false": 73 | break; 74 | case "require": 75 | case "required": 76 | ssl.enforce = true; 77 | goto case "yes"; 78 | case "1": 79 | case "yes": 80 | case "true": 81 | caps |= CapabilityFlags.CLIENT_SSL; 82 | break; 83 | default: 84 | throw new MySQLException(format("Bad value for 'ssl' on connection string: %s", value)); 85 | } 86 | break; 87 | case "ssl_rootcert": 88 | ssl.rootCertFile = value; 89 | break; 90 | case "ssl_hostname": 91 | ssl.hostName = value; 92 | break; 93 | case "ssl_ciphers": 94 | ssl.ciphers = value; 95 | break; 96 | case "ssl_version": 97 | switch (value) with (SSLConfig.Version) { 98 | case "any": 99 | ssl.sslVersion = any; 100 | break; 101 | case "ssl3": 102 | ssl.sslVersion = ssl3; 103 | break; 104 | case "tls1": 105 | ssl.sslVersion = tls1; 106 | break; 107 | case "tls1_1": 108 | ssl.sslVersion = tls1_1; 109 | break; 110 | case "tls1_2": 111 | ssl.sslVersion = tls1_2; 112 | break; 113 | case "dtls1": 114 | ssl.sslVersion = dtls1; 115 | break; 116 | default: 117 | throw new MySQLException(format("Bad value for 'ssl_version' on connection string: %s", value)); 118 | } 119 | break; 120 | case "ssl_validate": 121 | switch (value) with (SSLConfig.Validate) { 122 | case "basic": 123 | ssl.validate = basic; 124 | break; 125 | case "trust": 126 | ssl.validate = trust; 127 | break; 128 | case "identity": 129 | ssl.validate = identity; 130 | break; 131 | default: 132 | throw new MySQLException(format("Bad value for 'ssl_validate' on connection string: %s", value)); 133 | } 134 | break; 135 | default: 136 | throw new MySQLException(format("Bad connection string: %s", connectionString)); 137 | } 138 | 139 | if (indexValueEnd == remaining.length) 140 | return; 141 | 142 | remaining = remaining[indexValueEnd+1..$]; 143 | indexValue = remaining.indexOf("="); 144 | } 145 | 146 | throw new MySQLException(format("Bad connection string: %s", connectionString)); 147 | } 148 | 149 | CapabilityFlags caps = DefaultClientCaps; 150 | 151 | const(char)[] host; 152 | const(char)[] user; 153 | const(char)[] pwd; 154 | const(char)[] db; 155 | ushort port = 3306; 156 | 157 | SSLConfig ssl; 158 | } 159 | 160 | 161 | private struct ServerInfo { 162 | const(char)[] versionString; 163 | ubyte protocol; 164 | ubyte charSet; 165 | ushort status; 166 | uint connection; 167 | uint caps; 168 | } 169 | 170 | 171 | @property string placeholders(size_t x, bool parens = true) { 172 | if (x) { 173 | auto app = appender!string; 174 | if (parens) { 175 | app.reserve(x + x - 1); 176 | 177 | app.put('('); 178 | foreach (i; 0..x - 1) 179 | app.put("?,"); 180 | app.put('?'); 181 | app.put(')'); 182 | } else { 183 | app.reserve(x + x + 1); 184 | 185 | foreach (i; 0..x - 1) 186 | app.put("?,"); 187 | app.put('?'); 188 | } 189 | return app.data; 190 | } 191 | 192 | return null; 193 | } 194 | 195 | 196 | @property string placeholders(T)(T x, bool parens = true) if (is(typeof(() { auto y = x.length; }))) { 197 | return x.length.placeholders(parens); 198 | } 199 | 200 | 201 | struct PreparedStatement { 202 | package: 203 | uint id; 204 | uint params; 205 | } 206 | 207 | 208 | enum ConnectionOptions { 209 | TextProtocol = 1 << 0, // Execute method uses the MySQL text protocol under the hood - it's less safe but can increase performance in some situations 210 | TextProtocolCheckNoArgs = 1 << 1, // Check for orphan placeholders even if arguments are passed 211 | Default = 0 212 | } 213 | 214 | 215 | struct Connection(SocketType, ConnectionOptions Options = ConnectionOptions.Default) { 216 | void connect(string connectionString) { 217 | settings_ = ConnectionSettings(connectionString); 218 | connect(); 219 | } 220 | 221 | void connect(ConnectionSettings settings) { 222 | settings_ = settings; 223 | connect(); 224 | } 225 | 226 | void connect(const(char)[] host, ushort port, const(char)[] user, const(char)[] pwd, const(char)[] db, CapabilityFlags caps = DefaultClientCaps) { 227 | settings_.host = host; 228 | settings_.user = user; 229 | settings_.pwd = pwd; 230 | settings_.db = db; 231 | settings_.port = port; 232 | settings_.caps = caps | CapabilityFlags.CLIENT_LONG_PASSWORD | CapabilityFlags.CLIENT_PROTOCOL_41; 233 | 234 | connect(); 235 | } 236 | 237 | void use(const(char)[] db) { 238 | send(Commands.COM_INIT_DB, db); 239 | eatStatus(retrieve()); 240 | 241 | if ((caps_ & CapabilityFlags.CLIENT_SESSION_TRACK) == 0) { 242 | schema_.length = db.length; 243 | schema_[] = db[]; 244 | } 245 | } 246 | 247 | void ping() { 248 | send(Commands.COM_PING); 249 | eatStatus(retrieve()); 250 | } 251 | 252 | void refresh() { 253 | send(Commands.COM_REFRESH); 254 | eatStatus(retrieve()); 255 | } 256 | 257 | static if ((Options & ConnectionOptions.TextProtocol) == 0) { 258 | void reset() { 259 | send(Commands.COM_RESET_CONNECTION); 260 | eatStatus(retrieve()); 261 | } 262 | } 263 | 264 | const(char)[] statistics() { 265 | send(Commands.COM_STATISTICS); 266 | 267 | auto answer = retrieve(); 268 | return answer.eat!(const(char)[])(answer.remaining); 269 | } 270 | 271 | const(char)[] schema() const { 272 | return schema_; 273 | } 274 | 275 | ConnectionSettings settings() const { 276 | return settings_; 277 | } 278 | 279 | auto prepare(const(char)[] sql) { 280 | send(Commands.COM_STMT_PREPARE, sql); 281 | 282 | auto answer = retrieve(); 283 | 284 | if (answer.peek!ubyte != StatusPackets.OK_Packet) 285 | eatStatus(answer); 286 | 287 | answer.expect!ubyte(0); 288 | 289 | auto id = answer.eat!uint; 290 | auto columns = answer.eat!ushort; 291 | auto params = answer.eat!ushort; 292 | answer.expect!ubyte(0); 293 | 294 | auto warnings = answer.eat!ushort; 295 | 296 | if (params) { 297 | foreach (i; 0..params) 298 | skipColumnDef(retrieve(), Commands.COM_STMT_PREPARE); 299 | 300 | eatEOF(retrieve()); 301 | } 302 | 303 | if (columns) { 304 | foreach (i; 0..columns) 305 | skipColumnDef(retrieve(), Commands.COM_STMT_PREPARE); 306 | 307 | eatEOF(retrieve()); 308 | } 309 | 310 | return PreparedStatement(id, params); 311 | } 312 | 313 | void execute(Args...)(const(char)[] sql, Args args) { 314 | static if (Options & ConnectionOptions.TextProtocol) { 315 | query(sql, args); 316 | } else { 317 | scope(failure) disconnect_(); 318 | 319 | auto id = prepare(sql); 320 | execute(id, args); 321 | close(id); 322 | } 323 | } 324 | 325 | void set(T)(const(char)[] variable, T value) { 326 | query("set session ?=?", MySQLFragment(variable), value); 327 | } 328 | 329 | const(char)[] get(const(char)[] variable) { 330 | const(char)[] result; 331 | query("show session variables like ?", variable, (MySQLRow row) { 332 | result = row[1].peek!(const(char)[]).dup; 333 | }); 334 | 335 | return result; 336 | } 337 | 338 | void begin() { 339 | if (inTransaction) 340 | throw new MySQLErrorException("MySQL does not support nested transactions - commit or rollback before starting a new transaction"); 341 | 342 | query("start transaction"); 343 | 344 | assert(inTransaction); 345 | } 346 | 347 | void commit() { 348 | if (!inTransaction) 349 | throw new MySQLErrorException("No active transaction"); 350 | 351 | query("commit"); 352 | 353 | assert(!inTransaction); 354 | } 355 | 356 | void rollback() { 357 | if (connected) { 358 | if ((status_.flags & StatusFlags.SERVER_STATUS_IN_TRANS) == 0) 359 | throw new MySQLErrorException("No active transaction"); 360 | 361 | query("rollback"); 362 | 363 | assert(!inTransaction); 364 | } 365 | } 366 | 367 | @property bool inTransaction() const { 368 | return connected && (status_.flags & StatusFlags.SERVER_STATUS_IN_TRANS); 369 | } 370 | 371 | void execute(Args...)(PreparedStatement stmt, Args args) { 372 | scope(failure) disconnect_(); 373 | 374 | ensureConnected(); 375 | 376 | seq_ = 0; 377 | auto packet = OutputPacket(&out_); 378 | packet.put!ubyte(Commands.COM_STMT_EXECUTE); 379 | packet.put!uint(stmt.id); 380 | packet.put!ubyte(Cursors.CURSOR_TYPE_READ_ONLY); 381 | packet.put!uint(1); 382 | 383 | static if (args.length == 0) { 384 | enum shouldDiscard = true; 385 | } else { 386 | enum shouldDiscard = !isCallable!(args[args.length - 1]); 387 | } 388 | 389 | enum argCount = shouldDiscard ? args.length : (args.length - 1); 390 | 391 | if (!argCount && stmt.params) 392 | throw new MySQLErrorException(format("Wrong number of parameters for query. Got 0 but expected %d.", stmt.params)); 393 | 394 | static if (argCount) { 395 | enum NullsCapacity = 128; // must be power of 2 396 | ubyte[NullsCapacity >> 3] nulls; 397 | size_t bitsOut; 398 | size_t indexArg; 399 | foreach(i, arg; args[0..argCount]) { 400 | const auto index = (indexArg >> 3) & (NullsCapacity - 1); 401 | const auto bit = indexArg & 7; 402 | 403 | static if (is(typeof(arg) == typeof(null))) { 404 | nulls[index] = nulls[index] | (1 << bit); 405 | ++indexArg; 406 | } else static if (is(Unqual!(typeof(arg)) == MySQLValue)) { 407 | if (arg.isNull) 408 | nulls[index] = nulls[index] | (1 << bit); 409 | ++indexArg; 410 | } else static if (isArray!(typeof(arg)) && !isSomeString!(typeof(arg))) { 411 | indexArg += arg.length; 412 | } else { 413 | ++indexArg; 414 | } 415 | 416 | auto finishing = (i == argCount - 1); 417 | auto remaining = indexArg - bitsOut; 418 | 419 | if (finishing || (remaining >= NullsCapacity)) { 420 | while (remaining) { 421 | auto bits = min(remaining, NullsCapacity); 422 | 423 | packet.put(nulls[0..(bits + 7) >> 3]); 424 | bitsOut += bits; 425 | nulls[] = 0; 426 | 427 | remaining = (indexArg - bitsOut); 428 | if (!remaining || (!finishing && (remaining < NullsCapacity))) 429 | break; 430 | } 431 | } 432 | } 433 | packet.put!ubyte(1); 434 | 435 | if (indexArg != stmt.params) 436 | throw new MySQLErrorException(format("Wrong number of parameters for query. Got %d but expected %d.", indexArg, stmt.params)); 437 | 438 | foreach (arg; args[0..argCount]) { 439 | static if (is(typeof(arg) == enum)) { 440 | putValueType(packet, cast(OriginalType!(Unqual!(typeof(arg))))arg); 441 | } else { 442 | putValueType(packet, arg); 443 | } 444 | } 445 | 446 | foreach (arg; args[0..argCount]) { 447 | static if (is(typeof(arg) == enum)) { 448 | putValue(packet, cast(OriginalType!(Unqual!(typeof(arg))))arg); 449 | } else { 450 | putValue(packet, arg); 451 | } 452 | } 453 | } 454 | 455 | packet.finalize(seq_); 456 | ++seq_; 457 | 458 | socket_.write(packet.get()); 459 | 460 | auto answer = retrieve(); 461 | if (isStatus(answer)) { 462 | eatStatus(answer); 463 | } else { 464 | static if (!shouldDiscard) { 465 | resultSet(answer, stmt.id, Commands.COM_STMT_EXECUTE, args[args.length - 1]); 466 | } else { 467 | discardAll(answer, Commands.COM_STMT_EXECUTE); 468 | } 469 | } 470 | } 471 | 472 | void close(PreparedStatement stmt) { 473 | uint[1] data = [ stmt.id ]; 474 | send(Commands.COM_STMT_CLOSE, data); 475 | } 476 | 477 | alias OnStatusCallback = scope void delegate(ConnectionStatus status, const(char)[] message); 478 | @property void onStatus(OnStatusCallback callback) { 479 | onStatus_ = callback; 480 | } 481 | 482 | @property OnStatusCallback onStatus() const { 483 | return onStatus_; 484 | } 485 | 486 | alias OnDisconnectCallback = scope void delegate(ConnectionStatus status); 487 | @property void onDisconnect(OnDisconnectCallback callback) { 488 | onDisconnect_ = callback; 489 | } 490 | 491 | @property OnDisconnectCallback onDisconnect() const { 492 | return onDisconnect_; 493 | } 494 | 495 | @property ulong insertID() const { 496 | return status_.insertID; 497 | } 498 | 499 | @property ulong affected() const { 500 | return cast(size_t)status_.affected; 501 | } 502 | 503 | @property ulong matched() const { 504 | return cast(size_t)status_.matched; 505 | } 506 | 507 | @property ulong changed() const { 508 | return cast(size_t)status_.changed; 509 | } 510 | 511 | @property size_t warnings() const { 512 | return status_.warnings; 513 | } 514 | 515 | @property size_t error() const { 516 | return status_.error; 517 | } 518 | 519 | @property const(char)[] status() const { 520 | return info_; 521 | } 522 | 523 | @property bool connected() const { 524 | return socket_.connected; 525 | } 526 | 527 | void disconnect() { 528 | socket_.close(); 529 | } 530 | 531 | void reuse() { 532 | onDisconnect_ = null; 533 | onStatus_ = null; 534 | 535 | ensureConnected(); 536 | 537 | if (inTransaction) 538 | rollback; 539 | if (settings_.db.length && (settings_.db != schema_)) 540 | use(settings_.db); 541 | } 542 | 543 | @property void trace(bool enable) { 544 | trace_ = enable; 545 | } 546 | 547 | @property bool trace() { 548 | return trace_; 549 | } 550 | 551 | private: 552 | void disconnect_() { 553 | disconnect(); 554 | if (onDisconnect_ && error) 555 | onDisconnect_(status_); 556 | } 557 | 558 | void query(Args...)(const(char)[] sql, Args args) { 559 | scope(failure) disconnect_(); 560 | 561 | static if (args.length == 0) { 562 | enum shouldDiscard = true; 563 | } else { 564 | enum shouldDiscard = !isCallable!(args[args.length - 1]); 565 | } 566 | 567 | enum argCount = shouldDiscard ? args.length : (args.length - 1); 568 | 569 | static if (argCount || (Options & ConnectionOptions.TextProtocolCheckNoArgs)) { 570 | auto querySQL = prepareSQL(sql, args[0..argCount]); 571 | } else { 572 | auto querySQL = sql; 573 | } 574 | 575 | version(development) { 576 | import std.stdio : stderr, writefln; 577 | if (trace_) 578 | stderr.writefln("%s", querySQL); 579 | } 580 | 581 | send(Commands.COM_QUERY, querySQL); 582 | 583 | auto answer = retrieve(); 584 | if (isStatus(answer)) { 585 | eatStatus(answer); 586 | } else { 587 | static if (!shouldDiscard) { 588 | resultSetText(answer, Commands.COM_QUERY, args[args.length - 1]); 589 | } else { 590 | discardAll(answer, Commands.COM_QUERY); 591 | } 592 | } 593 | } 594 | 595 | void connect() { 596 | socket_.connect(settings_.host, settings_.port); 597 | 598 | seq_ = 0; 599 | eatHandshake(retrieve()); 600 | } 601 | 602 | void send(T)(Commands cmd, T[] data) { 603 | send(cmd, cast(ubyte*)data.ptr, data.length * T.sizeof); 604 | } 605 | 606 | void send(Commands cmd, ubyte* data = null, size_t length = 0) { 607 | ensureConnected(); 608 | 609 | seq_ = 0; 610 | auto header = OutputPacket(&out_); 611 | header.put!ubyte(cmd); 612 | header.finalize(seq_, length); 613 | ++seq_; 614 | 615 | socket_.write(header.get()); 616 | if (length) 617 | socket_.write(data[0..length]); 618 | } 619 | 620 | void ensureConnected() { 621 | if (!socket_.connected) 622 | connect(); 623 | } 624 | 625 | bool isStatus(InputPacket packet) { 626 | auto id = packet.peek!ubyte; 627 | switch (id) { 628 | case StatusPackets.ERR_Packet: 629 | case StatusPackets.OK_Packet: 630 | return true; 631 | default: 632 | return false; 633 | } 634 | } 635 | 636 | void check(InputPacket packet, bool smallError = false) { 637 | auto id = packet.peek!ubyte; 638 | switch (id) { 639 | case StatusPackets.ERR_Packet: 640 | case StatusPackets.OK_Packet: 641 | eatStatus(packet, smallError); 642 | break; 643 | default: 644 | break; 645 | } 646 | } 647 | 648 | InputPacket retrieve() { 649 | scope(failure) disconnect_(); 650 | 651 | ubyte[4] header; 652 | socket_.read(header); 653 | 654 | auto len = header.ptr[0] | (header.ptr[1] << 8) | (header.ptr[2] << 16); 655 | auto seq = header.ptr[3]; 656 | 657 | if (seq != seq_) 658 | throw new MySQLConnectionException("Out of order packet received"); 659 | 660 | ++seq_; 661 | 662 | in_.length = len; 663 | socket_.read(in_); 664 | 665 | if (in_.length != len) 666 | throw new MySQLConnectionException("Wrong number of bytes read"); 667 | 668 | return InputPacket(&in_); 669 | } 670 | 671 | void eatHandshake(InputPacket packet) { 672 | scope(failure) disconnect_(); 673 | 674 | check(packet, true); 675 | 676 | server_.protocol = packet.eat!ubyte; 677 | server_.versionString = packet.eat!(const(char)[])(packet.countUntil(0, true)).dup; 678 | packet.skip(1); 679 | 680 | server_.connection = packet.eat!uint; 681 | 682 | const auto authLengthStart = 8; 683 | size_t authLength = authLengthStart; 684 | 685 | ubyte[256] auth; 686 | auth[0..authLength] = packet.eat!(ubyte[])(authLength); 687 | 688 | packet.expect!ubyte(0); 689 | 690 | server_.caps = packet.eat!ushort; 691 | 692 | if (!packet.empty) { 693 | server_.charSet = packet.eat!ubyte; 694 | server_.status = packet.eat!ushort; 695 | server_.caps |= packet.eat!ushort << 16; 696 | server_.caps |= CapabilityFlags.CLIENT_LONG_PASSWORD; 697 | 698 | if ((server_.caps & CapabilityFlags.CLIENT_PROTOCOL_41) == 0) 699 | throw new MySQLProtocolException("Server doesn't support protocol v4.1"); 700 | 701 | if (server_.caps & CapabilityFlags.CLIENT_SECURE_CONNECTION) { 702 | packet.skip(1); 703 | } else { 704 | packet.expect!ubyte(0); 705 | } 706 | 707 | packet.skip(10); 708 | 709 | authLength += packet.countUntil(0, true); 710 | if (authLength > auth.length) 711 | throw new MySQLConnectionException("Bad packet format"); 712 | 713 | auth[authLengthStart..authLength] = packet.eat!(ubyte[])(authLength - authLengthStart); 714 | 715 | packet.expect!ubyte(0); 716 | } 717 | 718 | caps_ = cast(CapabilityFlags)(settings_.caps & server_.caps); 719 | 720 | if (((settings_.caps & CapabilityFlags.CLIENT_SSL) != 0) || settings_.ssl.enforce) { 721 | if ((caps_ & CapabilityFlags.CLIENT_SSL) != 0) { 722 | startSSL(); 723 | } else if (settings_.ssl.enforce) { 724 | throw new MySQLProtocolException("Server doesn't support SSL"); 725 | } 726 | } 727 | 728 | ubyte[20] token; 729 | { 730 | import std.digest.sha : sha1Of; 731 | 732 | auto pass = sha1Of(cast(const(ubyte)[])settings_.pwd); 733 | token = sha1Of(auth[0..authLength], sha1Of(pass)); 734 | 735 | foreach (i; 0..20) 736 | token[i] = token[i] ^ pass[i]; 737 | } 738 | 739 | auto reply = OutputPacket(&out_); 740 | 741 | reply.reserve(64 + settings_.user.length + settings_.pwd.length + settings_.db.length); 742 | 743 | reply.put!uint(caps_); 744 | reply.put!uint(1); 745 | reply.put!ubyte(45); 746 | reply.fill(0, 23); 747 | 748 | reply.put(settings_.user); 749 | reply.put!ubyte(0); 750 | 751 | if (settings_.pwd.length) { 752 | if (caps_ & CapabilityFlags.CLIENT_SECURE_CONNECTION) { 753 | reply.put!ubyte(token.length); 754 | reply.put(token); 755 | } else { 756 | reply.put(token); 757 | reply.put!ubyte(0); 758 | } 759 | } else { 760 | reply.put!ubyte(0); 761 | } 762 | 763 | if ((settings_.db.length || schema_.length) && (caps_ & CapabilityFlags.CLIENT_CONNECT_WITH_DB)) { 764 | if (schema_.length) { 765 | reply.put(schema_); 766 | } else { 767 | reply.put(settings_.db); 768 | 769 | schema_.length = settings_.db.length; 770 | schema_[] = settings_.db[]; 771 | } 772 | } 773 | 774 | reply.put!ubyte(0); 775 | 776 | reply.finalize(seq_); 777 | ++seq_; 778 | 779 | socket_.write(reply.get()); 780 | 781 | eatStatus(retrieve()); 782 | } 783 | 784 | 785 | void startSSL() { 786 | auto request = OutputPacket(&out_); 787 | 788 | request.reserve(64); 789 | 790 | request.put!uint(caps_); 791 | request.put!uint(1); 792 | request.put!ubyte(45); 793 | request.fill(0, 23); 794 | 795 | request.finalize(seq_); 796 | ++seq_; 797 | 798 | socket_.write(request.get()); 799 | 800 | socket_.startSSL(settings_.host, settings_.ssl); 801 | } 802 | 803 | 804 | void eatStatus(InputPacket packet, bool smallError = false) { 805 | auto id = packet.eat!ubyte; 806 | 807 | switch (id) { 808 | case StatusPackets.OK_Packet: 809 | status_.matched = 0; 810 | status_.changed = 0; 811 | status_.affected = packet.eatLenEnc(); 812 | status_.insertID = packet.eatLenEnc(); 813 | status_.flags = packet.eat!ushort; 814 | if (caps_ & CapabilityFlags.CLIENT_PROTOCOL_41) 815 | status_.warnings = packet.eat!ushort; 816 | status_.error = 0; 817 | info([]); 818 | 819 | if (caps_ & CapabilityFlags.CLIENT_SESSION_TRACK) { 820 | if (!packet.empty) { 821 | info(packet.eat!(const(char)[])(packet.eatLenEnc())); 822 | 823 | if (status_.flags & StatusFlags.SERVER_SESSION_STATE_CHANGED) { 824 | packet.skipLenEnc(); 825 | while (!packet.empty()) { 826 | final switch (packet.eat!ubyte()) with (SessionStateType) { 827 | case SESSION_TRACK_SCHEMA: 828 | packet.skipLenEnc(); 829 | schema_.length = packet.eatLenEnc(); 830 | schema_[] = packet.eat!(const(char)[])(schema_.length); 831 | break; 832 | case SESSION_TRACK_SYSTEM_VARIABLES: 833 | case SESSION_TRACK_GTIDS: 834 | case SESSION_TRACK_STATE_CHANGE: 835 | case SESSION_TRACK_TRANSACTION_STATE: 836 | case SESSION_TRACK_TRANSACTION_CHARACTERISTICS: 837 | packet.skip(packet.eatLenEnc()); 838 | break; 839 | } 840 | } 841 | } 842 | } 843 | } else { 844 | info(packet.eat!(const(char)[])(packet.remaining)); 845 | } 846 | 847 | import std.regex : matchFirst, regex; 848 | static matcher = regex(`\smatched:\s*(\d+)\s+changed:\s*(\d+)`, `i`); 849 | auto matches = matchFirst(info_, matcher); 850 | if (!matches.empty) { 851 | status_.matched = matches[1].to!ulong; 852 | status_.changed = matches[2].to!ulong; 853 | } 854 | 855 | if (onStatus_) 856 | onStatus_(status_, info_); 857 | 858 | break; 859 | case StatusPackets.EOF_Packet: 860 | status_.affected = 0; 861 | status_.changed = 0; 862 | status_.matched = 0; 863 | status_.error = 0; 864 | status_.warnings = packet.eat!ushort; 865 | status_.flags = packet.eat!ushort; 866 | info([]); 867 | 868 | if (onStatus_) 869 | onStatus_(status_, info_); 870 | 871 | break; 872 | case StatusPackets.ERR_Packet: 873 | status_.affected = 0; 874 | status_.changed = 0; 875 | status_.matched = 0; 876 | status_.flags = 0; 877 | status_.warnings = 0; 878 | status_.error = packet.eat!ushort; 879 | if (!smallError) 880 | packet.skip(6); 881 | info(packet.eat!(const(char)[])(packet.remaining)); 882 | 883 | if (onStatus_) 884 | onStatus_(status_, info_); 885 | 886 | switch(status_.error) { 887 | case ErrorCodes.ER_DUP_ENTRY_WITH_KEY_NAME: 888 | case ErrorCodes.ER_DUP_ENTRY: 889 | throw new MySQLDuplicateEntryException(info_.idup); 890 | case ErrorCodes.ER_DATA_TOO_LONG_FOR_COL: 891 | throw new MySQLDataTooLongException(info_.idup); 892 | case ErrorCodes.ER_DEADLOCK_FOUND: 893 | throw new MySQLDeadlockFoundException(info_.idup); 894 | case ErrorCodes.ER_TABLE_DOESNT_EXIST: 895 | throw new MySQLTableDoesntExistException(info_.idup); 896 | case ErrorCodes.ER_LOCK_WAIT_TIMEOUT: 897 | throw new MySQLLockWaitTimeoutException(info_.idup); 898 | default: 899 | version(development) { 900 | // On dev show the query together with the error message 901 | throw new MySQLErrorException(format("[err:%s] %s - %s", status_.error, info_, sql_.data)); 902 | } else { 903 | throw new MySQLErrorException(format("[err:%s] %s", status_.error, info_)); 904 | } 905 | } 906 | default: 907 | throw new MySQLProtocolException("Unexpected packet format"); 908 | } 909 | } 910 | 911 | void info(const(char)[] value) { 912 | info_.length = value.length; 913 | info_[0..$] = value; 914 | } 915 | 916 | void skipColumnDef(InputPacket packet, Commands cmd) { 917 | packet.skip(cast(size_t)packet.eatLenEnc()); // catalog 918 | packet.skip(cast(size_t)packet.eatLenEnc()); // schema 919 | packet.skip(cast(size_t)packet.eatLenEnc()); // table 920 | packet.skip(cast(size_t)packet.eatLenEnc()); // original_table 921 | packet.skip(cast(size_t)packet.eatLenEnc()); // name 922 | packet.skip(cast(size_t)packet.eatLenEnc()); // original_name 923 | packet.skipLenEnc(); // next_length 924 | packet.skip(10); // 2 + 4 + 1 + 2 + 1 // charset, length, type, flags, decimals 925 | packet.expect!ushort(0); 926 | 927 | if (cmd == Commands.COM_FIELD_LIST) 928 | packet.skip(cast(size_t)packet.eatLenEnc());// default values 929 | } 930 | 931 | void columnDef(InputPacket packet, Commands cmd, ref MySQLColumn def) { 932 | packet.skip(cast(size_t)packet.eatLenEnc()); // catalog 933 | packet.skip(cast(size_t)packet.eatLenEnc()); // schema 934 | packet.skip(cast(size_t)packet.eatLenEnc()); // table 935 | packet.skip(cast(size_t)packet.eatLenEnc()); // original_table 936 | auto len = cast(size_t)packet.eatLenEnc(); 937 | columns_ ~= packet.eat!(const(char)[])(len); 938 | def.name = columns_[$-len..$]; 939 | packet.skip(cast(size_t)packet.eatLenEnc()); // original_name 940 | packet.skipLenEnc(); // next_length 941 | packet.skip(2); // charset 942 | def.length = packet.eat!uint; 943 | def.type = cast(ColumnTypes)packet.eat!ubyte; 944 | def.flags = packet.eat!ushort; 945 | def.decimals = packet.eat!ubyte; 946 | 947 | packet.expect!ushort(0); 948 | 949 | if (cmd == Commands.COM_FIELD_LIST) 950 | packet.skip(cast(size_t)packet.eatLenEnc());// default values 951 | } 952 | 953 | void columnDefs(size_t count, Commands cmd, ref MySQLColumn[] defs) { 954 | defs.length = count; 955 | foreach (i; 0..count) 956 | columnDef(retrieve(), cmd, defs[i]); 957 | } 958 | 959 | bool callHandler(RowHandler)(RowHandler handler, size_t, MySQLHeader, MySQLRow row) if ((ParameterTypeTuple!(RowHandler).length == 1) && is(ParameterTypeTuple!(RowHandler)[0] == MySQLRow)) { 960 | static if (is(ReturnType!(RowHandler) == void)) { 961 | handler(row); 962 | return true; 963 | } else { 964 | return handler(row); // return type must be bool 965 | } 966 | } 967 | 968 | bool callHandler(RowHandler)(RowHandler handler, size_t i, MySQLHeader, MySQLRow row) if ((ParameterTypeTuple!(RowHandler).length == 2) && isNumeric!(ParameterTypeTuple!(RowHandler)[0]) && is(ParameterTypeTuple!(RowHandler)[1] == MySQLRow)) { 969 | static if (is(ReturnType!(RowHandler) == void)) { 970 | handler(cast(ParameterTypeTuple!(RowHandler)[0])i, row); 971 | return true; 972 | } else { 973 | return handler(cast(ParameterTypeTuple!(RowHandler)[0])i, row); // return type must be bool 974 | } 975 | } 976 | 977 | bool callHandler(RowHandler)(RowHandler handler, size_t, MySQLHeader header, MySQLRow row) if ((ParameterTypeTuple!(RowHandler).length == 2) && is(ParameterTypeTuple!(RowHandler)[0] == MySQLHeader) && is(ParameterTypeTuple!(RowHandler)[1] == MySQLRow)) { 978 | static if (is(ReturnType!(RowHandler) == void)) { 979 | handler(header, row); 980 | return true; 981 | } else { 982 | return handler(header, row); // return type must be bool 983 | } 984 | } 985 | 986 | bool callHandler(RowHandler)(RowHandler handler, size_t i, MySQLHeader header, MySQLRow row) if ((ParameterTypeTuple!(RowHandler).length == 3) && isNumeric!(ParameterTypeTuple!(RowHandler)[0]) && is(ParameterTypeTuple!(RowHandler)[1] == MySQLHeader) && is(ParameterTypeTuple!(RowHandler)[2] == MySQLRow)) { 987 | static if (is(ReturnType!(RowHandler) == void)) { 988 | handler(i, header, row); 989 | return true; 990 | } else { 991 | return handler(i, header, row); // return type must be bool 992 | } 993 | } 994 | 995 | void resultSetRow(InputPacket packet, MySQLHeader header, ref MySQLRow row) { 996 | assert(row.columns.length == header.length); 997 | 998 | packet.expect!ubyte(0); 999 | auto nulls = packet.eat!(ubyte[])((header.length + 2 + 7) >> 3); 1000 | foreach (i, ref column; header) { 1001 | const auto index = (i + 2) >> 3; // bit offset of 2 1002 | const auto bit = (i + 2) & 7; 1003 | 1004 | if ((nulls[index] & (1 << bit)) == 0) { 1005 | eatValue(packet, column, row.get_(i)); 1006 | } else { 1007 | auto signed = (column.flags & FieldFlags.UNSIGNED_FLAG) == 0; 1008 | row.get_(i) = MySQLValue(column.name, ColumnTypes.MYSQL_TYPE_NULL, signed, null, 0); 1009 | } 1010 | } 1011 | assert(packet.empty); 1012 | } 1013 | 1014 | void resultSet(RowHandler)(InputPacket packet, uint stmt, Commands cmd, RowHandler handler) { 1015 | columns_.length = 0; 1016 | 1017 | auto columns = cast(size_t)packet.eatLenEnc(); 1018 | columnDefs(columns, cmd, header_); 1019 | row_.header_(header_); 1020 | 1021 | auto status = retrieve(); 1022 | if (status.peek!ubyte == StatusPackets.ERR_Packet) 1023 | eatStatus(status); 1024 | 1025 | size_t index; 1026 | auto statusFlags = eatEOF(status); 1027 | if (statusFlags & StatusFlags.SERVER_STATUS_CURSOR_EXISTS) { 1028 | uint[2] data = [ stmt, 4096 ]; // todo: make setting - rows per fetch 1029 | while (statusFlags & (StatusFlags.SERVER_STATUS_CURSOR_EXISTS | StatusFlags.SERVER_MORE_RESULTS_EXISTS)) { 1030 | send(Commands.COM_STMT_FETCH, data); 1031 | 1032 | auto answer = retrieve(); 1033 | if (answer.peek!ubyte == StatusPackets.ERR_Packet) 1034 | eatStatus(answer); 1035 | 1036 | auto row = answer.empty ? retrieve() : answer; 1037 | while (true) { 1038 | if (row.peek!ubyte == StatusPackets.EOF_Packet) { 1039 | statusFlags = eatEOF(row); 1040 | break; 1041 | } 1042 | 1043 | resultSetRow(row, header_, row_); 1044 | if (!callHandler(handler, index++, header_, row_)) { 1045 | discardUntilEOF(retrieve()); 1046 | statusFlags = 0; 1047 | break; 1048 | } 1049 | row = retrieve(); 1050 | } 1051 | } 1052 | } else { 1053 | while (true) { 1054 | auto row = retrieve(); 1055 | if (row.peek!ubyte == StatusPackets.EOF_Packet) { 1056 | eatEOF(row); 1057 | break; 1058 | } 1059 | 1060 | resultSetRow(row, header_, row_); 1061 | if (!callHandler(handler, index++, header_, row_)) { 1062 | discardUntilEOF(retrieve()); 1063 | break; 1064 | } 1065 | } 1066 | } 1067 | } 1068 | 1069 | void resultSetRowText(InputPacket packet, MySQLHeader header, ref MySQLRow row) { 1070 | assert(row.columns.length == header.length); 1071 | 1072 | foreach(i, ref column; header) { 1073 | if (packet.peek!ubyte != 0xfb) { 1074 | eatValueText(packet, column, row.get_(i)); 1075 | } else { 1076 | packet.skip(1); 1077 | auto signed = (column.flags & FieldFlags.UNSIGNED_FLAG) == 0; 1078 | row.get_(i) = MySQLValue(column.name, ColumnTypes.MYSQL_TYPE_NULL, signed, null, 0); 1079 | } 1080 | } 1081 | assert(packet.empty); 1082 | } 1083 | 1084 | void resultSetText(RowHandler)(InputPacket packet, Commands cmd, RowHandler handler) { 1085 | columns_.length = 0; 1086 | 1087 | auto columns = cast(size_t)packet.eatLenEnc(); 1088 | columnDefs(columns, cmd, header_); 1089 | row_.header_(header_); 1090 | 1091 | eatEOF(retrieve()); 1092 | 1093 | size_t index; 1094 | while (true) { 1095 | auto row = retrieve(); 1096 | if (row.peek!ubyte == StatusPackets.EOF_Packet) { 1097 | eatEOF(row); 1098 | break; 1099 | } else if (row.peek!ubyte == StatusPackets.ERR_Packet) { 1100 | eatStatus(row); 1101 | break; 1102 | } 1103 | 1104 | resultSetRowText(row, header_, row_); 1105 | if (!callHandler(handler, index++, header_, row_)) { 1106 | discardUntilEOF(retrieve()); 1107 | break; 1108 | } 1109 | } 1110 | } 1111 | 1112 | void discardAll(InputPacket packet, Commands cmd) { 1113 | auto columns = cast(size_t)packet.eatLenEnc(); 1114 | columnDefs(columns, cmd, header_); 1115 | 1116 | auto statusFlags = eatEOF(retrieve()); 1117 | if ((statusFlags & StatusFlags.SERVER_STATUS_CURSOR_EXISTS) == 0) { 1118 | while (true) { 1119 | auto row = retrieve(); 1120 | if (row.peek!ubyte == StatusPackets.EOF_Packet) { 1121 | eatEOF(row); 1122 | break; 1123 | } 1124 | } 1125 | } 1126 | } 1127 | 1128 | void discardUntilEOF(InputPacket packet) { 1129 | while (true) { 1130 | if (packet.peek!ubyte == StatusPackets.EOF_Packet) { 1131 | eatEOF(packet); 1132 | break; 1133 | } 1134 | packet = retrieve(); 1135 | } 1136 | } 1137 | 1138 | auto eatEOF(InputPacket packet) { 1139 | auto id = packet.eat!ubyte; 1140 | if (id != StatusPackets.EOF_Packet) 1141 | throw new MySQLProtocolException("Unexpected packet format"); 1142 | 1143 | status_.error = 0; 1144 | status_.warnings = packet.eat!ushort(); 1145 | status_.flags = packet.eat!ushort(); 1146 | info([]); 1147 | 1148 | if (onStatus_) 1149 | onStatus_(status_, info_); 1150 | 1151 | return status_.flags; 1152 | } 1153 | 1154 | auto estimateArgs(Args...)(ref size_t estimated, Args args) { 1155 | size_t argCount; 1156 | 1157 | foreach(i, arg; args) { 1158 | static if (is(typeof(arg) == typeof(null))) { 1159 | ++argCount; 1160 | estimated += 4; 1161 | } else static if (is(Unqual!(typeof(arg)) == MySQLValue)) { 1162 | ++argCount; 1163 | final switch(arg.type) with (ColumnTypes) { 1164 | case MYSQL_TYPE_NULL: 1165 | estimated += 4; 1166 | break; 1167 | case MYSQL_TYPE_TINY: 1168 | estimated += 4; 1169 | break; 1170 | case MYSQL_TYPE_YEAR: 1171 | case MYSQL_TYPE_SHORT: 1172 | estimated += 6; 1173 | break; 1174 | case MYSQL_TYPE_INT24: 1175 | case MYSQL_TYPE_LONG: 1176 | estimated += 6; 1177 | break; 1178 | case MYSQL_TYPE_LONGLONG: 1179 | estimated += 8; 1180 | break; 1181 | case MYSQL_TYPE_FLOAT: 1182 | estimated += 8; 1183 | break; 1184 | case MYSQL_TYPE_DOUBLE: 1185 | estimated += 8; 1186 | break; 1187 | case MYSQL_TYPE_SET: 1188 | case MYSQL_TYPE_ENUM: 1189 | case MYSQL_TYPE_VARCHAR: 1190 | case MYSQL_TYPE_VAR_STRING: 1191 | case MYSQL_TYPE_STRING: 1192 | case MYSQL_TYPE_JSON: 1193 | case MYSQL_TYPE_NEWDECIMAL: 1194 | case MYSQL_TYPE_DECIMAL: 1195 | case MYSQL_TYPE_TINY_BLOB: 1196 | case MYSQL_TYPE_MEDIUM_BLOB: 1197 | case MYSQL_TYPE_LONG_BLOB: 1198 | case MYSQL_TYPE_BLOB: 1199 | case MYSQL_TYPE_BIT: 1200 | case MYSQL_TYPE_GEOMETRY: 1201 | estimated += 2 + arg.peek!(const(char)[]).length; 1202 | break; 1203 | case MYSQL_TYPE_TIME: 1204 | case MYSQL_TYPE_TIME2: 1205 | estimated += 18; 1206 | break; 1207 | case MYSQL_TYPE_DATE: 1208 | case MYSQL_TYPE_NEWDATE: 1209 | case MYSQL_TYPE_DATETIME: 1210 | case MYSQL_TYPE_DATETIME2: 1211 | case MYSQL_TYPE_TIMESTAMP: 1212 | case MYSQL_TYPE_TIMESTAMP2: 1213 | estimated += 20; 1214 | break; 1215 | } 1216 | } else static if (isArray!(typeof(arg)) && !isSomeString!(typeof(arg))) { 1217 | argCount += arg.length; 1218 | estimated += arg.length * 6; 1219 | } else static if (isSomeString!(typeof(arg)) || is(Unqual!(typeof(arg)) == MySQLRawString) || is(Unqual!(typeof(arg)) == MySQLFragment) || is(Unqual!(typeof(arg)) == MySQLBinary)) { 1220 | ++argCount; 1221 | estimated += 2 + arg.length; 1222 | } else { 1223 | ++argCount; 1224 | estimated += 6; 1225 | } 1226 | } 1227 | return argCount; 1228 | } 1229 | 1230 | auto prepareSQL(Args...)(const(char)[] sql, Args args) { 1231 | auto estimated = sql.length; 1232 | auto argCount = estimateArgs(estimated, args); 1233 | 1234 | sql_.clear; 1235 | sql_.reserve(max(8192, estimated)); 1236 | 1237 | alias AppendFunc = bool function(ref Appender!(char[]), ref const(char)[] sql, ref size_t, const(void)*) @safe pure nothrow; 1238 | AppendFunc[Args.length] funcs; 1239 | const(void)*[Args.length] addrs; 1240 | 1241 | foreach (i, Arg; Args) { 1242 | static if (is(Arg == enum)) { 1243 | funcs[i] = () @trusted { return cast(AppendFunc)&appendNextValue!(OriginalType!Arg); }(); 1244 | addrs[i] = (ref x) @trusted { return cast(const void*)&x; }(cast(OriginalType!(Unqual!Arg))args[i]); 1245 | } else { 1246 | funcs[i] = () @trusted { return cast(AppendFunc)&appendNextValue!(Arg); }(); 1247 | addrs[i] = (ref x) @trusted { return cast(const void*)&x; }(args[i]); 1248 | } 1249 | } 1250 | 1251 | size_t indexArg; 1252 | foreach (i; 0..Args.length) { 1253 | if (!funcs[i](sql_, sql, indexArg, addrs[i])) 1254 | throw new MySQLErrorException(format("Wrong number of parameters for query. Got %d but expected %d.", argCount, indexArg)); 1255 | } 1256 | 1257 | finishCopy(sql_, sql, argCount, indexArg); 1258 | 1259 | return sql_.data; 1260 | } 1261 | 1262 | void finishCopy(ref Appender!(char[]) app, ref const(char)[] sql, size_t argCount, size_t indexArg) { 1263 | if (copyUpToNext(sql_, sql)) { 1264 | ++indexArg; 1265 | while (copyUpToNext(sql_, sql)) 1266 | ++indexArg; 1267 | throw new MySQLErrorException(format("Wrong number of parameters for query. Got %d but expected %d.", argCount, indexArg)); 1268 | } 1269 | } 1270 | 1271 | SocketType socket_; 1272 | MySQLHeader header_; 1273 | MySQLRow row_; 1274 | char[] columns_; 1275 | char[] info_; 1276 | char[] schema_; 1277 | ubyte[] in_; 1278 | ubyte[] out_; 1279 | ubyte seq_; 1280 | Appender!(char[]) sql_; 1281 | 1282 | OnStatusCallback onStatus_; 1283 | OnDisconnectCallback onDisconnect_; 1284 | CapabilityFlags caps_; 1285 | ConnectionStatus status_; 1286 | ConnectionSettings settings_; 1287 | ServerInfo server_; 1288 | 1289 | // For tracing queries 1290 | bool trace_; 1291 | } 1292 | 1293 | private auto copyUpToNext(ref Appender!(char[]) app, ref const(char)[] sql) { 1294 | size_t offset; 1295 | dchar quote = '\0'; 1296 | 1297 | while (offset < sql.length) { 1298 | auto ch = decode!(UseReplacementDchar.no)(sql, offset); 1299 | switch (ch) { 1300 | case '?': 1301 | if (!quote) { 1302 | app.put(sql[0..offset - 1]); 1303 | sql = sql[offset..$]; 1304 | return true; 1305 | } else { 1306 | goto default; 1307 | } 1308 | case '\'': 1309 | case '\"': 1310 | case '`': 1311 | if (quote == ch) { 1312 | quote = '\0'; 1313 | } else if (!quote) { 1314 | quote = ch; 1315 | } 1316 | goto default; 1317 | case '\\': 1318 | if (quote && (offset < sql.length)) 1319 | decode!(UseReplacementDchar.no)(sql, offset); 1320 | goto default; 1321 | default: 1322 | break; 1323 | } 1324 | } 1325 | app.put(sql[0..offset]); 1326 | sql = sql[offset..$]; 1327 | return false; 1328 | } 1329 | 1330 | private bool appendNextValue(T)(ref Appender!(char[]) app, ref const(char)[] sql, ref size_t indexArg, const(void)* arg) { 1331 | static if (isArray!T && !isSomeString!(OriginalType!T)) { 1332 | foreach (i, ref v; *cast(T*)arg) { 1333 | if (copyUpToNext(app, sql)) { 1334 | appendValue(app, v); 1335 | ++indexArg; 1336 | } else { 1337 | return false; 1338 | } 1339 | } 1340 | } else { 1341 | if (copyUpToNext(app, sql)) { 1342 | appendValue(app, *cast(T*)arg); 1343 | ++indexArg; 1344 | } else { 1345 | return false; 1346 | } 1347 | } 1348 | return true; 1349 | } 1350 | -------------------------------------------------------------------------------- /src/mysql/exception.d: -------------------------------------------------------------------------------- 1 | module mysql.exception; 2 | 3 | 4 | class MySQLException : Exception { 5 | this(string msg, string file = __FILE__, size_t line = __LINE__) pure { 6 | super(msg, file, line); 7 | } 8 | } 9 | 10 | class MySQLConnectionException: MySQLException { 11 | this(string msg, string file = __FILE__, size_t line = __LINE__) pure { 12 | super(msg, file, line); 13 | } 14 | } 15 | 16 | class MySQLProtocolException: MySQLException { 17 | this(string msg, string file = __FILE__, size_t line = __LINE__) pure { 18 | super(msg, file, line); 19 | } 20 | } 21 | 22 | class MySQLErrorException : Exception { 23 | this(string msg, string file = __FILE__, size_t line = __LINE__) pure { 24 | super(msg, file, line); 25 | } 26 | } 27 | 28 | class MySQLDuplicateEntryException : MySQLErrorException { 29 | this(string msg, string file = __FILE__, size_t line = __LINE__) pure { 30 | super(msg, file, line); 31 | } 32 | } 33 | 34 | class MySQLDataTooLongException : MySQLErrorException { 35 | this(string msg, string file = __FILE__, size_t line = __LINE__) pure { 36 | super(msg, file, line); 37 | } 38 | } 39 | 40 | class MySQLDeadlockFoundException : MySQLErrorException { 41 | this(string msg, string file = __FILE__, size_t line = __LINE__) pure { 42 | super(msg, file, line); 43 | } 44 | } 45 | 46 | class MySQLTableDoesntExistException : MySQLErrorException { 47 | this(string msg, string file = __FILE__, size_t line = __LINE__) pure { 48 | super(msg, file, line); 49 | } 50 | } 51 | 52 | class MySQLLockWaitTimeoutException : MySQLErrorException { 53 | this(string msg, string file = __FILE__, size_t line = __LINE__) pure { 54 | super(msg, file, line); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/mysql/inserter.d: -------------------------------------------------------------------------------- 1 | module mysql.inserter; 2 | 3 | 4 | import std.array; 5 | import std.meta; 6 | import std.range; 7 | import std.string; 8 | import std.traits; 9 | 10 | 11 | import mysql.appender; 12 | import mysql.exception; 13 | import mysql.type; 14 | 15 | enum OnDuplicate : size_t { 16 | Ignore, 17 | Error, 18 | Replace, 19 | Update, 20 | UpdateAll, 21 | } 22 | 23 | auto inserter(ConnectionType)(auto ref ConnectionType connection) { 24 | return Inserter!ConnectionType(connection); 25 | } 26 | 27 | 28 | auto inserter(ConnectionType, Args...)(auto ref ConnectionType connection, OnDuplicate action, string tableName, Args columns) { 29 | auto insert = Inserter!ConnectionType(&connection); 30 | insert.start(action, tableName, columns); 31 | return insert; 32 | } 33 | 34 | 35 | auto inserter(ConnectionType, Args...)(auto ref ConnectionType connection, string tableName, Args columns) { 36 | auto insert = Inserter!ConnectionType(&connection); 37 | insert.start(OnDuplicate.Error, tableName, columns); 38 | return insert; 39 | } 40 | 41 | 42 | private template isSomeStringOrSomeStringArray(T) { 43 | enum isSomeStringOrSomeStringArray = isSomeString!(OriginalType!T) || (isArray!T && isSomeString!(ElementType!T)); 44 | } 45 | 46 | 47 | struct Inserter(ConnectionType) { 48 | @disable this(); 49 | @disable this(this); 50 | 51 | this(ConnectionType* connection) { 52 | conn_ = connection; 53 | pending_ = 0; 54 | flushes_ = 0; 55 | } 56 | 57 | ~this() { 58 | flush(); 59 | } 60 | 61 | void start(Args...)(string tableName, Args fieldNames) if (Args.length && allSatisfy!(isSomeStringOrSomeStringArray, Args)) { 62 | start(OnDuplicate.Error, tableName, fieldNames); 63 | } 64 | 65 | void start(Args...)(OnDuplicate action, string tableName, Args fieldNames) if (Args.length && allSatisfy!(isSomeStringOrSomeStringArray, Args)) { 66 | auto fieldCount = fieldNames.length; 67 | 68 | foreach (size_t i, Arg; Args) { 69 | static if (isArray!Arg && !isSomeString!(OriginalType!Arg)) { 70 | fieldCount = (fieldCount - 1) + fieldNames[i].length; 71 | } 72 | } 73 | 74 | fields_ = fieldCount; 75 | 76 | Appender!(char[]) app; 77 | 78 | final switch(action) with (OnDuplicate) { 79 | case Ignore: 80 | app.put("insert ignore into "); 81 | break; 82 | case Replace: 83 | app.put("replace into "); 84 | break; 85 | case UpdateAll: 86 | Appender!(char[]) dupapp; 87 | 88 | foreach(size_t i, Arg; Args) { 89 | static if (isSomeString!(OriginalType!Arg)) { 90 | dupapp.put('`'); 91 | dupapp.put(fieldNames[i]); 92 | dupapp.put("`=values(`"); 93 | dupapp.put(fieldNames[i]); 94 | dupapp.put("`)"); 95 | } else { 96 | auto columns = fieldNames[i]; 97 | foreach (j, name; columns) { 98 | dupapp.put('`'); 99 | dupapp.put(name); 100 | dupapp.put("`=values(`"); 101 | dupapp.put(name); 102 | dupapp.put("`)"); 103 | if (j + 1 != columns.length) 104 | dupapp.put(','); 105 | } 106 | } 107 | if (i + 1 != Args.length) 108 | dupapp.put(','); 109 | } 110 | dupUpdate_ = dupapp.data; 111 | goto case Update; 112 | case Update: 113 | case Error: 114 | app.put("insert into "); 115 | break; 116 | } 117 | 118 | app.put(tableName); 119 | app.put('('); 120 | 121 | foreach (size_t i, Arg; Args) { 122 | static if (isSomeString!(OriginalType!Arg)) { 123 | fieldsHash_ ~= hashOf(fieldNames[i]); 124 | fieldsNames_ ~= fieldNames[i]; 125 | 126 | app.put('`'); 127 | app.put(fieldNames[i]); 128 | app.put('`'); 129 | } else { 130 | auto columns = fieldNames[i]; 131 | foreach (j, name; columns) { 132 | 133 | fieldsHash_ ~= hashOf(name); 134 | fieldsNames_ ~= name; 135 | 136 | app.put('`'); 137 | app.put(name); 138 | app.put('`'); 139 | if (j + 1 != columns.length) 140 | app.put(','); 141 | } 142 | } 143 | if (i + 1 != Args.length) 144 | app.put(','); 145 | } 146 | 147 | app.put(")values"); 148 | start_ = app.data; 149 | } 150 | 151 | auto ref duplicateUpdate(string update) { 152 | dupUpdate_ = cast(char[])update; 153 | return this; 154 | } 155 | 156 | void rows(T)(ref const T[] param) if (!isValueType!T) { 157 | if (param.length < 1) 158 | return; 159 | 160 | foreach (ref p; param) 161 | row(p); 162 | } 163 | 164 | private auto tryAppendField(string member, string parentMembers = "", T)(ref const T param, ref size_t fieldHash, ref bool fieldFound) { 165 | static if (isReadableDataMember!(Unqual!T, member)) { 166 | alias memberType = typeof(__traits(getMember, param, member)); 167 | static if (isValueType!(memberType)) { 168 | static if (getUDAs!(__traits(getMember, param, member), NameAttribute).length){ 169 | enum nameHash = hashOf(parentMembers~getUDAs!(__traits(getMember, param, member), NameAttribute)[0].name); 170 | } 171 | else { 172 | enum nameHash = hashOf(parentMembers~member); 173 | } 174 | if (nameHash == fieldHash || (parentMembers == "" && getUDAs!(T, UnCamelCaseAttribute).length && hashOf(member.unCamelCase) == fieldHash)) { 175 | appendValue(values_, __traits(getMember, param, member)); 176 | fieldFound = true; 177 | return; 178 | } 179 | } else { 180 | foreach (subMember; __traits(allMembers, memberType)) { 181 | static if (parentMembers == "") { 182 | tryAppendField!(subMember, member~".")(__traits(getMember, param, member), fieldHash, fieldFound); 183 | } else { 184 | tryAppendField!(subMember, parentMembers~member~".")(__traits(getMember, param, member), fieldHash, fieldFound); 185 | } 186 | 187 | if (fieldFound) 188 | return; 189 | } 190 | } 191 | } 192 | } 193 | 194 | void row (T) (ref const T param) if (!isValueType!T) { 195 | scope (failure) reset(); 196 | 197 | if (start_.empty) 198 | throw new MySQLErrorException("Inserter must be initialized with a call to start()"); 199 | 200 | if (!pending_) 201 | values_.put(cast(char[])start_); 202 | 203 | values_.put(pending_ ? ",(" : "("); 204 | ++pending_; 205 | 206 | bool fieldFound; 207 | foreach (i, ref fieldHash; fieldsHash_) { 208 | fieldFound = false; 209 | foreach (member; __traits(allMembers, T)) { 210 | tryAppendField!member(param, fieldHash, fieldFound); 211 | if (fieldFound) 212 | break; 213 | } 214 | if (!fieldFound) 215 | throw new MySQLErrorException(format("field '%s' was not found in struct => '%s' members", fieldsNames_.ptr[i], typeid(Unqual!T).name)); 216 | 217 | if (i != fields_-1) 218 | values_.put(','); 219 | } 220 | values_.put(')'); 221 | 222 | if (values_.data.length > (128 << 10)) // todo: make parameter 223 | flush(); 224 | 225 | ++rows_; 226 | } 227 | 228 | void row(Values...)(Values values) if(allSatisfy!(isValueType, Values)) { 229 | 230 | scope(failure) reset(); 231 | 232 | if (start_.empty) 233 | throw new MySQLErrorException("Inserter must be initialized with a call to start()"); 234 | 235 | auto valueCount = values.length; 236 | 237 | foreach (size_t i, Value; Values) { 238 | static if (isArray!Value && !isSomeString!(OriginalType!Value)) { 239 | valueCount = (valueCount - 1) + values[i].length; 240 | } 241 | } 242 | 243 | if (valueCount != fields_) 244 | throw new MySQLErrorException(format("Wrong number of parameters for row. Got %d but expected %d.", valueCount, fields_)); 245 | 246 | if (!pending_) 247 | values_.put(cast(char[])start_); 248 | 249 | values_.put(pending_ ? ",(" : "("); 250 | ++pending_; 251 | foreach (size_t i, Value; Values) { 252 | static if (isArray!Value && !isSomeString!(OriginalType!Value)) { 253 | appendValues(values_, values[i]); 254 | } else { 255 | appendValue(values_, values[i]); 256 | } 257 | if (i != values.length-1) 258 | values_.put(','); 259 | } 260 | values_.put(')'); 261 | 262 | if (values_.data.length > bufferSize_) 263 | flush(); 264 | 265 | ++rows_; 266 | } 267 | 268 | 269 | 270 | @property size_t rows() const { 271 | return rows_ != 0; 272 | } 273 | 274 | @property size_t pending() const { 275 | return pending_ != 0; 276 | } 277 | 278 | @property size_t flushes() const { 279 | return flushes_; 280 | } 281 | 282 | @property void bufferSize(size_t size) { 283 | bufferSize_ = size; 284 | } 285 | 286 | @property size_t bufferSize() const { 287 | return bufferSize_; 288 | } 289 | 290 | private void reset(){ 291 | values_.clear; 292 | pending_ = 0; 293 | } 294 | 295 | 296 | void flush() { 297 | if (pending_) { 298 | if (dupUpdate_.length) { 299 | values_.put(cast(ubyte[])" on duplicate key update "); 300 | values_.put(cast(ubyte[])dupUpdate_); 301 | } 302 | 303 | auto sql = cast(char[])values_.data(); 304 | reset(); 305 | 306 | conn_.execute(sql); 307 | ++flushes_; 308 | } 309 | } 310 | 311 | private: 312 | char[] start_; 313 | char[] dupUpdate_; 314 | Appender!(char[]) values_; 315 | 316 | ConnectionType* conn_; 317 | size_t pending_; 318 | size_t flushes_; 319 | size_t fields_; 320 | size_t rows_; 321 | string[] fieldsNames_; 322 | size_t[] fieldsHash_; 323 | size_t bufferSize_ = (128 << 10); 324 | } 325 | -------------------------------------------------------------------------------- /src/mysql/package.d: -------------------------------------------------------------------------------- 1 | module mysql; 2 | 3 | 4 | public import mysql.appender; 5 | public import mysql.client; 6 | public import mysql.exception; 7 | public import mysql.inserter; 8 | public import mysql.protocol; 9 | public import mysql.row; 10 | public import mysql.socket; 11 | public import mysql.type; 12 | -------------------------------------------------------------------------------- /src/mysql/packet.d: -------------------------------------------------------------------------------- 1 | module mysql.packet; 2 | 3 | 4 | import std.algorithm; 5 | import std.traits; 6 | 7 | import mysql.exception; 8 | 9 | 10 | struct InputPacket { 11 | @disable this(); 12 | 13 | this(ubyte[]* buffer) { 14 | buffer_ = buffer; 15 | in_ = *buffer_; 16 | } 17 | 18 | T peek(T)() if (!isArray!T) { 19 | assert(T.sizeof <= in_.length); 20 | return *(cast(T*)in_.ptr); 21 | } 22 | 23 | T eat(T)() if (!isArray!T) { 24 | assert(T.sizeof <= in_.length); 25 | auto ptr = cast(T*)in_.ptr; 26 | in_ = in_[T.sizeof..$]; 27 | return *ptr; 28 | } 29 | 30 | T peek(T)(size_t count) if (isArray!T) { 31 | alias ValueType = typeof(Type.init[0]); 32 | 33 | assert(ValueType.sizeof * count <= in_.length); 34 | auto ptr = cast(ValueType*)in_.ptr; 35 | return ptr[0..count]; 36 | } 37 | 38 | T eat(T)(size_t count) if (isArray!T) { 39 | alias ValueType = typeof(T.init[0]); 40 | 41 | assert(ValueType.sizeof * count <= in_.length); 42 | auto ptr = cast(ValueType*)in_.ptr; 43 | in_ = in_[ValueType.sizeof * count..$]; 44 | return ptr[0..count]; 45 | } 46 | 47 | void expect(T)(T x) { 48 | if (x != eat!T) 49 | throw new MySQLProtocolException("Bad packet format"); 50 | } 51 | 52 | void skip(size_t count) { 53 | assert(count <= in_.length); 54 | in_ = in_[count..$]; 55 | } 56 | 57 | auto countUntil(ubyte x, bool expect) { 58 | auto index = in_.countUntil(x); 59 | if (expect) { 60 | if ((index < 0) || (in_[index] != x)) 61 | throw new MySQLProtocolException("Bad packet format"); 62 | } 63 | return index; 64 | } 65 | 66 | void skipLenEnc() { 67 | auto header = eat!ubyte; 68 | if (header >= 0xfb) { 69 | switch(header) { 70 | case 0xfb: 71 | return; 72 | case 0xfc: 73 | skip(2); 74 | return; 75 | case 0xfd: 76 | skip(3); 77 | return; 78 | case 0xfe: 79 | skip(8); 80 | return; 81 | default: 82 | throw new MySQLProtocolException("Bad packet format"); 83 | } 84 | } 85 | } 86 | 87 | ulong eatLenEnc() { 88 | auto header = eat!ubyte; 89 | if (header < 0xfb) 90 | return header; 91 | 92 | ulong lo; 93 | ulong hi; 94 | 95 | switch(header) { 96 | case 0xfb: 97 | return 0; 98 | case 0xfc: 99 | return eat!ushort; 100 | case 0xfd: 101 | lo = eat!ubyte; 102 | hi = eat!ushort; 103 | return lo | (hi << 8); 104 | case 0xfe: 105 | lo = eat!uint; 106 | hi = eat!uint; 107 | return lo | (hi << 32); 108 | default: 109 | throw new MySQLProtocolException("Bad packet format"); 110 | } 111 | } 112 | 113 | auto remaining() const { 114 | return in_.length; 115 | } 116 | 117 | bool empty() const { 118 | return in_.length == 0; 119 | } 120 | protected: 121 | ubyte[]* buffer_; 122 | ubyte[] in_; 123 | } 124 | 125 | 126 | struct OutputPacket { 127 | @disable this(); 128 | 129 | this(ubyte[]* buffer) { 130 | buffer_ = buffer; 131 | out_ = buffer_.ptr + 4; 132 | } 133 | 134 | pragma(inline, true) void put(T)(T x) { 135 | put(offset_, x); 136 | } 137 | 138 | void put(T)(size_t offset, T x) if (!isArray!T) { 139 | grow(offset, T.sizeof); 140 | 141 | *(cast(T*)(out_ + offset)) = x; 142 | offset_ = max(offset + T.sizeof, offset_); 143 | } 144 | 145 | void put(T)(size_t offset, T x) if (isArray!T) { 146 | alias ValueType = Unqual!(typeof(T.init[0])); 147 | 148 | grow(offset, ValueType.sizeof * x.length); 149 | 150 | (cast(ValueType*)(out_ + offset))[0..x.length] = x; 151 | offset_ = max(offset + (ValueType.sizeof * x.length), offset_); 152 | } 153 | 154 | void putLenEnc(ulong x) { 155 | if (x < 0xfb) { 156 | put!ubyte(cast(ubyte)x); 157 | } else if (x <= ushort.max) { 158 | put!ubyte(0xfc); 159 | put!ushort(cast(ushort)x); 160 | } else if (x <= (uint.max >> 8)) { 161 | put!ubyte(0xfd); 162 | put!ubyte(cast(ubyte)(x)); 163 | put!ushort(cast(ushort)(x >> 8)); 164 | } else { 165 | put!ubyte(0xfe); 166 | put!uint(cast(uint)x); 167 | put!uint(cast(uint)(x >> 32)); 168 | } 169 | } 170 | 171 | size_t marker(T)() if (!isArray!T) { 172 | grow(offset_, T.sizeof); 173 | 174 | auto place = offset_; 175 | offset_ += T.sizeof; 176 | return place; 177 | } 178 | 179 | size_t marker(T)(size_t count) if (isArray!T) { 180 | alias ValueType = Unqual!(typeof(T.init[0])); 181 | grow(offset_, ValueType.sizeof * x.length); 182 | 183 | auto place = offset_; 184 | offset_ += (ValueType.sizeof * x.length); 185 | return place; 186 | } 187 | 188 | void finalize(ubyte seq) { 189 | if (offset_ >= 0xffffff) 190 | throw new MySQLConnectionException("Packet size exceeds 2^24"); 191 | uint length = cast(uint)offset_; 192 | uint header = cast(uint)((offset_ & 0xffffff) | (seq << 24)); 193 | *(cast(uint*)buffer_.ptr) = header; 194 | } 195 | 196 | void finalize(ubyte seq, size_t extra) { 197 | if (offset_ + extra >= 0xffffff) 198 | throw new MySQLConnectionException("Packet size exceeds 2^24"); 199 | uint length = cast(uint)(offset_ + extra); 200 | uint header = cast(uint)((length & 0xffffff) | (seq << 24)); 201 | *(cast(uint*)buffer_.ptr) = header; 202 | } 203 | 204 | void reset() { 205 | offset_ = 0; 206 | } 207 | 208 | void reserve(size_t size) { 209 | (*buffer_).length = max((*buffer_).length, 4 + size); 210 | out_ = buffer_.ptr + 4; 211 | } 212 | 213 | void fill(ubyte x, size_t size) { 214 | grow(offset_, size); 215 | out_[offset_..offset_ + size] = 0; 216 | offset_ += size; 217 | } 218 | 219 | size_t length() const { 220 | return offset_; 221 | } 222 | 223 | bool empty() const { 224 | return offset_ == 0; 225 | } 226 | 227 | const(ubyte)[] get() const { 228 | return (*buffer_)[0..4 + offset_]; 229 | } 230 | protected: 231 | void grow(size_t offset, size_t size) { 232 | auto requested = 4 + offset + size; 233 | if (requested > buffer_.length) { 234 | auto capacity = max(128, (*buffer_).capacity); 235 | while (capacity < requested) 236 | capacity <<= 1; 237 | buffer_.length = capacity; 238 | out_ = buffer_.ptr + 4; 239 | } 240 | } 241 | ubyte[]* buffer_; 242 | ubyte* out_; 243 | size_t offset_; 244 | } 245 | -------------------------------------------------------------------------------- /src/mysql/protocol.d: -------------------------------------------------------------------------------- 1 | module mysql.protocol; 2 | 3 | 4 | enum CapabilityFlags : uint { 5 | CLIENT_LONG_PASSWORD = 0x00000001, // Use the improved version of Old Password Authentication 6 | CLIENT_FOUND_ROWS = 0x00000002, // Send found rows instead of affected rows in EOF_Packet 7 | CLIENT_LONG_FLAG = 0x00000004, // Longer flags in Protocol::ColumnDefinition320 8 | CLIENT_CONNECT_WITH_DB = 0x00000008, // One can specify db on connect in Handshake Response Packet 9 | CLIENT_NO_SCHEMA = 0x00000010, // Don't allow database.table.column 10 | CLIENT_COMPRESS = 0x00000020, // Compression protocol supported 11 | CLIENT_ODBC = 0x00000040, // Special handling of ODBC behaviour 12 | CLIENT_LOCAL_FILES = 0x00000080, // Can use LOAD DATA LOCAL 13 | CLIENT_IGNORE_SPACE = 0x00000100, // Parser can ignore spaces before '(' 14 | CLIENT_PROTOCOL_41 = 0x00000200, // Supports the 4.1 protocol 15 | CLIENT_INTERACTIVE = 0x00000400, // wait_timeout vs. wait_interactive_timeout 16 | CLIENT_SSL = 0x00000800, // Supports SSL 17 | CLIENT_IGNORE_SIGPIPE = 0x00001000, // Don't issue SIGPIPE if network failures (libmysqlclient only) 18 | CLIENT_TRANSACTIONS = 0x00002000, // Can send status flags in EOF_Packet 19 | CLIENT_RESERVED = 0x00004000, // Unused 20 | CLIENT_SECURE_CONNECTION = 0x00008000, // Supports Authentication::Native41 21 | CLIENT_MULTI_STATEMENTS = 0x00010000, // Can handle multiple statements per COM_QUERY and COM_STMT_PREPARE 22 | CLIENT_MULTI_RESULTS = 0x00020000, // Can send multiple resultsets for COM_QUERY 23 | CLIENT_PS_MULTI_RESULTS = 0x00040000, // Can send multiple resultsets for COM_STMT_EXECUTE 24 | CLIENT_PLUGIN_AUTH = 0x00080000, // Sends extra data in Initial Handshake Packet and supports the pluggable authentication protocol. 25 | CLIENT_CONNECT_ATTRS = 0x00100000, // Allows connection attributes in Protocol::HandshakeResponse41 26 | CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA = 0x00200000, // Understands length encoded integer for auth response data in Protocol::HandshakeResponse41 27 | CLIENT_CAN_HANDLE_EXPIRED_PASSWORDS = 0x00400000, // Announces support for expired password extension 28 | CLIENT_SESSION_TRACK = 0x00800000, // Can set SERVER_SESSION_STATE_CHANGED in the Status Flags and send session-state change data after a OK packet 29 | CLIENT_DEPRECATE_EOF = 0x01000000, // Can send OK after a Text Resultset 30 | } 31 | 32 | 33 | enum StatusFlags : ushort { 34 | SERVER_STATUS_IN_TRANS = 0x0001, // A transaction is active 35 | SERVER_STATUS_AUTOCOMMIT = 0x0002, // auto-commit is enabled 36 | SERVER_MORE_RESULTS_EXISTS = 0x0008, 37 | SERVER_STATUS_NO_GOOD_INDEX_USED = 0x0010, 38 | SERVER_STATUS_NO_INDEX_USED = 0x0020, 39 | SERVER_STATUS_CURSOR_EXISTS = 0x0040, // Used by Binary Protocol Resultset to signal that COM_STMT_FETCH has to be used to fetch the row-data. 40 | SERVER_STATUS_LAST_ROW_SENT = 0x0080, 41 | SERVER_STATUS_DB_DROPPED = 0x0100, 42 | SERVER_STATUS_NO_BACKSLASH_ESCAPES = 0x0200, 43 | SERVER_STATUS_METADATA_CHANGED = 0x0400, 44 | SERVER_QUERY_WAS_SLOW = 0x0800, 45 | SERVER_PS_OUT_PARAMS = 0x1000, 46 | SERVER_STATUS_IN_TRANS_READONLY = 0x2000, // In a read-only transaction 47 | SERVER_SESSION_STATE_CHANGED = 0x4000, // connection state information has changed 48 | } 49 | 50 | 51 | enum SessionStateType : ubyte { 52 | SESSION_TRACK_SYSTEM_VARIABLES, 53 | SESSION_TRACK_SCHEMA, 54 | SESSION_TRACK_STATE_CHANGE, 55 | SESSION_TRACK_GTIDS, 56 | SESSION_TRACK_TRANSACTION_CHARACTERISTICS, 57 | SESSION_TRACK_TRANSACTION_STATE 58 | } 59 | 60 | 61 | enum StatusPackets : ubyte { 62 | OK_Packet = 0, 63 | ERR_Packet = 0xff, 64 | EOF_Packet = 0xfe, 65 | } 66 | 67 | 68 | enum Commands : ubyte { 69 | //COM_SLEEP = 0x00, 70 | COM_QUIT = 0x01, 71 | COM_INIT_DB = 0x02, 72 | COM_QUERY = 0x03, 73 | COM_FIELD_LIST = 0x04, 74 | COM_CREATE_DB = 0x05, 75 | COM_DROP_DB = 0x06, 76 | COM_REFRESH = 0x07, 77 | //COM_SHUTDOWN = 0x08, 78 | COM_STATISTICS = 0x09, 79 | COM_PROCESS_INFO = 0x0a, 80 | //COM_CONNECT = 0x0b, 81 | COM_PROCESS_KILL = 0x0c, 82 | COM_DEBUG = 0x0d, 83 | COM_PING = 0x0e, 84 | //COM_TIME = 0x0f, 85 | //COM_DELAYED_INSERT = 0x10, 86 | COM_CHANGE_USER = 0x11, 87 | COM_BINLOG_DUMP = 0x12, 88 | COM_TABLE_DUMP = 0x13, 89 | //COM_CONNECT_OUT = 0x14, 90 | COM_REGISTER_SLAVE = 0x15, 91 | COM_STMT_PREPARE = 0x16, 92 | COM_STMT_EXECUTE = 0x17, 93 | COM_STMT_SEND_LONG_DATA = 0x18, 94 | COM_STMT_CLOSE = 0x19, 95 | COM_STMT_RESET = 0x1a, 96 | COM_SET_OPTION = 0x1b, 97 | COM_STMT_FETCH = 0x1c, 98 | //COM_DAEMON = 0x1d, 99 | COM_BINLOG_DUMP_GTID = 0x1e, 100 | COM_RESET_CONNECTION = 0x1f, 101 | } 102 | 103 | 104 | enum Cursors : ubyte { 105 | CURSOR_TYPE_NO_CURSOR = 0x00, 106 | CURSOR_TYPE_READ_ONLY = 0x01, 107 | CURSOR_TYPE_FOR_UPDATE = 0x02, 108 | CURSOR_TYPE_SCROLLABLE = 0x04, 109 | } 110 | 111 | 112 | enum ColumnTypes : ubyte { 113 | MYSQL_TYPE_DECIMAL = 0x00, 114 | MYSQL_TYPE_TINY = 0x01, 115 | MYSQL_TYPE_SHORT = 0x02, 116 | MYSQL_TYPE_LONG = 0x03, 117 | MYSQL_TYPE_FLOAT = 0x04, 118 | MYSQL_TYPE_DOUBLE = 0x05, 119 | MYSQL_TYPE_NULL = 0x06, 120 | MYSQL_TYPE_TIMESTAMP = 0x07, 121 | MYSQL_TYPE_LONGLONG = 0x08, 122 | MYSQL_TYPE_INT24 = 0x09, 123 | MYSQL_TYPE_DATE = 0x0a, 124 | MYSQL_TYPE_TIME = 0x0b, 125 | MYSQL_TYPE_DATETIME = 0x0c, 126 | MYSQL_TYPE_YEAR = 0x0d, 127 | MYSQL_TYPE_NEWDATE = 0x0e, 128 | MYSQL_TYPE_VARCHAR = 0x0f, 129 | MYSQL_TYPE_BIT = 0x10, 130 | MYSQL_TYPE_TIMESTAMP2 = 0x11, 131 | MYSQL_TYPE_DATETIME2 = 0x12, 132 | MYSQL_TYPE_TIME2 = 0x13, 133 | MYSQL_TYPE_JSON = 0xf5, 134 | MYSQL_TYPE_NEWDECIMAL = 0xf6, 135 | MYSQL_TYPE_ENUM = 0xf7, 136 | MYSQL_TYPE_SET = 0xf8, 137 | MYSQL_TYPE_TINY_BLOB = 0xf9, 138 | MYSQL_TYPE_MEDIUM_BLOB = 0xfa, 139 | MYSQL_TYPE_LONG_BLOB = 0xfb, 140 | MYSQL_TYPE_BLOB = 0xfc, 141 | MYSQL_TYPE_VAR_STRING = 0xfd, 142 | MYSQL_TYPE_STRING = 0xfe, 143 | MYSQL_TYPE_GEOMETRY = 0xff, 144 | } 145 | 146 | 147 | auto columnTypeName(ColumnTypes type) { 148 | final switch (type) with (ColumnTypes) { 149 | case MYSQL_TYPE_DECIMAL: return "decimal"; 150 | case MYSQL_TYPE_TINY: return "tiny"; 151 | case MYSQL_TYPE_SHORT: return "short"; 152 | case MYSQL_TYPE_LONG: return "long"; 153 | case MYSQL_TYPE_FLOAT: return "float"; 154 | case MYSQL_TYPE_DOUBLE: return "double"; 155 | case MYSQL_TYPE_NULL: return "null"; 156 | case MYSQL_TYPE_TIMESTAMP: return "timestamp"; 157 | case MYSQL_TYPE_LONGLONG: return "longlong"; 158 | case MYSQL_TYPE_INT24: return "int24"; 159 | case MYSQL_TYPE_DATE: return "date"; 160 | case MYSQL_TYPE_TIME: return "time"; 161 | case MYSQL_TYPE_DATETIME: return "datetime"; 162 | case MYSQL_TYPE_YEAR: return "year"; 163 | case MYSQL_TYPE_NEWDATE: return "newdate"; 164 | case MYSQL_TYPE_VARCHAR: return "varchar"; 165 | case MYSQL_TYPE_BIT: return "bit"; 166 | case MYSQL_TYPE_TIMESTAMP2: return "timestamp2"; 167 | case MYSQL_TYPE_DATETIME2: return "datetime2"; 168 | case MYSQL_TYPE_TIME2: return "time2"; 169 | case MYSQL_TYPE_JSON: return "json"; 170 | case MYSQL_TYPE_NEWDECIMAL: return "newdecimal"; 171 | case MYSQL_TYPE_ENUM: return "enum"; 172 | case MYSQL_TYPE_SET: return "set"; 173 | case MYSQL_TYPE_TINY_BLOB: return "tiny_blob"; 174 | case MYSQL_TYPE_MEDIUM_BLOB:return "medium_blob"; 175 | case MYSQL_TYPE_LONG_BLOB: return "long_blob"; 176 | case MYSQL_TYPE_BLOB: return "blob"; 177 | case MYSQL_TYPE_VAR_STRING: return "var_string"; 178 | case MYSQL_TYPE_STRING: return "string"; 179 | case MYSQL_TYPE_GEOMETRY: return "geometry"; 180 | } 181 | } 182 | 183 | 184 | enum FieldFlags : ushort { 185 | NOT_NULL_FLAG = 0x0001, // Field cannot be NULL 186 | PRI_KEY_FLAG = 0x0002, // Field is part of a primary key 187 | UNIQUE_KEY_FLAG = 0x0004, // Field is part of a unique key 188 | MULTIPLE_KEY_FLAG = 0x0008, // Field is part of a nonunique key 189 | BLOB_FLAG = 0x0010, // Field is a BLOB or TEXT (deprecated) 190 | UNSIGNED_FLAG = 0x0020, // Field has the UNSIGNED attribute 191 | ZEROFILL_FLAG = 0x0040, // Field has the ZEROFILL attribute 192 | BINARY_FLAG = 0x0080, // Field has the BINARY attribute 193 | ENUM_FLAG = 0x0100, // Field is an ENUM 194 | AUTO_INCREMENT_FLAG = 0x0200, // Field has the AUTO_INCREMENT attribute 195 | TIMESTAMP_FLAG = 0x0400, // Field is a TIMESTAMP (deprecated) 196 | SET_FLAG = 0x0800, // Field is a SET 197 | NO_DEFAULT_VALUE_FLAG = 0x1000, // Field has no default value; see additional notes following table 198 | ON_UPDATE_NOW_FLAG = 0x2000, // Field is set to NOW on UPDATE 199 | // PART_KEY_FLAG = 0x4000, // Intern; Part of some key 200 | NUM_FLAG = 0x8000, // Field is numeric 201 | } 202 | 203 | 204 | enum ErrorCodes : ushort { 205 | ER_DUP_KEYNAME = 1061, 206 | ER_DUP_ENTRY = 1062, 207 | ER_DUP_ENTRY_WITH_KEY_NAME = 1586, 208 | ER_DEADLOCK_FOUND = 1213, 209 | ER_DATA_TOO_LONG_FOR_COL = 1406, 210 | ER_TABLE_DOESNT_EXIST = 1146, 211 | ER_LOCK_WAIT_TIMEOUT = 1205, 212 | } 213 | -------------------------------------------------------------------------------- /src/mysql/row.d: -------------------------------------------------------------------------------- 1 | module mysql.row; 2 | 3 | 4 | import std.algorithm; 5 | import std.datetime; 6 | import std.traits; 7 | import std.typecons; 8 | static import std.ascii; 9 | import std.format : format; 10 | 11 | import mysql.exception; 12 | import mysql.type; 13 | 14 | enum Strict { 15 | yes = 0, 16 | yesIgnoreNull, 17 | no, 18 | } 19 | 20 | 21 | private uint hashOf(const(char)[] x) { 22 | uint hash = 2166136261u; 23 | foreach(i; 0..x.length) 24 | hash = (hash ^ cast(uint)(std.ascii.toLower(x.ptr[i]))) * 16777619u; 25 | return hash; 26 | } 27 | 28 | 29 | private bool equalsCI(const(char)[]x, const(char)[] y) { 30 | if (x.length != y.length) 31 | return false; 32 | 33 | foreach(i; 0..x.length) { 34 | if (std.ascii.toLower(x.ptr[i]) != std.ascii.toLower(y.ptr[i])) 35 | return false; 36 | } 37 | 38 | return true; 39 | } 40 | 41 | 42 | struct MySQLRow { 43 | @property size_t opDollar() const { 44 | return values_.length; 45 | } 46 | 47 | @property const(const(char)[])[] columns() const { 48 | return names_; 49 | } 50 | 51 | @property ref auto opDispatch(string key)() const { 52 | enum hash = hashOf(key); 53 | return dispatchFast_(hash, key); 54 | } 55 | 56 | auto opSlice() const { 57 | return values_; 58 | } 59 | 60 | auto opSlice(size_t i, size_t j) const { 61 | return values_[i..j]; 62 | } 63 | 64 | ref auto opIndex(string key) const { 65 | if (auto index = find_(key.hashOf, key)) 66 | return values_[index - 1]; 67 | throw new MySQLErrorException("Column '" ~ key ~ "' was not found in this result set"); 68 | } 69 | 70 | ref auto opIndex(size_t index) const { 71 | return values_[index]; 72 | } 73 | 74 | const(MySQLValue)* opBinaryRight(string op)(string key) const if (op == "in") { 75 | if (auto index = find(key.hashOf, key)) 76 | return &values_[index - 1]; 77 | return null; 78 | } 79 | 80 | int opApply(int delegate(const ref MySQLValue value) del) const { 81 | foreach (ref v; values_) 82 | if (auto ret = del(v)) 83 | return ret; 84 | return 0; 85 | } 86 | 87 | int opApply(int delegate(ref size_t, const ref MySQLValue) del) const { 88 | foreach (ref size_t i, ref v; values_) 89 | if (auto ret = del(i, v)) 90 | return ret; 91 | return 0; 92 | } 93 | 94 | int opApply(int delegate(const ref const(char)[], const ref MySQLValue) del) const { 95 | foreach (size_t i, ref v; values_) 96 | if (auto ret = del(names_[i], v)) 97 | return ret; 98 | return 0; 99 | } 100 | 101 | void toString(Appender)(ref Appender app) const { 102 | import std.format : formattedWrite; 103 | formattedWrite(&app, "%s", values_); 104 | } 105 | 106 | string toString() const { 107 | import std.conv : to; 108 | return to!string(values_); 109 | } 110 | 111 | string[] toStringArray(size_t start = 0, size_t end = ~cast(size_t)0) const { 112 | end = min(end, values_.length); 113 | start = min(start, values_.length); 114 | if (start > end) 115 | swap(start, end); 116 | 117 | string[] result; 118 | result.reserve(end - start); 119 | foreach(i; start..end) 120 | result ~= values_[i].toString; 121 | return result; 122 | } 123 | 124 | void toStruct(T, Strict strict = Strict.yesIgnoreNull)(ref T x) if(is(Unqual!T == struct) && !is(T == Strict)) { 125 | static if (isTuple!(Unqual!T)) { 126 | foreach(i, ref f; x.field) { 127 | if (i < values_.length) { 128 | static if (strict != Strict.yes) { 129 | if (!this[i].isNull) 130 | f = this[i].get!(Unqual!(typeof(f))); 131 | } else { 132 | f = this[i].get!(Unqual!(typeof(f))); 133 | } 134 | } 135 | else static if ((strict == Strict.yes) || (strict == Strict.yesIgnoreNull)) { 136 | throw new MySQLErrorException("Column " ~ i ~ " is out of range for this result set"); 137 | } 138 | } 139 | } else { 140 | structurize!(strict, null)(x); 141 | } 142 | } 143 | 144 | void toStruct(Strict strict, T)(ref T x) if (is(Unqual!T == struct)) { 145 | toStruct!(T, strict)(x); 146 | } 147 | 148 | T toStruct(T, Strict strict = Strict.yesIgnoreNull)() if (is(Unqual!T == struct)) { 149 | T result; 150 | toStruct!(T, strict)(result); 151 | return result; 152 | } 153 | 154 | package: 155 | ref auto dispatchFast_(uint hash, string key) const { 156 | if (auto index = find_(hash, key)) 157 | return opIndex(index - 1); 158 | throw new MySQLErrorException("Column '" ~ key ~ "' was not found in this result set"); 159 | } 160 | 161 | void header_(MySQLHeader header) { 162 | auto headerLen = header.length; 163 | auto idealLen = (headerLen + (headerLen >> 2)); 164 | auto indexLen = index_.length; 165 | 166 | index_[] = 0; 167 | 168 | if (indexLen < idealLen) { 169 | indexLen = max(32, indexLen); 170 | 171 | while (indexLen < idealLen) 172 | indexLen <<= 1; 173 | 174 | index_.length = indexLen; 175 | } 176 | 177 | auto mask = (indexLen - 1); 178 | assert((indexLen & mask) == 0); 179 | 180 | names_.length = headerLen; 181 | values_.length = headerLen; 182 | foreach (index, ref column; header) { 183 | names_[index] = column.name; 184 | 185 | auto hash = hashOf(column.name) & mask; 186 | auto probe = 1; 187 | 188 | while (true) { 189 | if (index_[hash] == 0) { 190 | index_[hash] = cast(uint)index + 1; 191 | break; 192 | } 193 | 194 | hash = (hash + probe++) & mask; 195 | } 196 | } 197 | } 198 | 199 | uint find_(uint hash, const(char)[] key) const { 200 | if (auto mask = index_.length - 1) { 201 | assert((index_.length & mask) == 0); 202 | 203 | hash = hash & mask; 204 | auto probe = 1; 205 | 206 | while (true) { 207 | auto index = index_[hash]; 208 | if (index) { 209 | if (names_[index - 1].equalsCI(key)) 210 | return index; 211 | hash = (hash + probe++) & mask; 212 | } else { 213 | break; 214 | } 215 | } 216 | } 217 | return 0; 218 | } 219 | 220 | ref auto get_(size_t index) { 221 | return values_[index]; 222 | } 223 | 224 | private: 225 | void structurize(Strict strict = Strict.yesIgnoreNull, string path = null, T)(ref T result) { 226 | enum unCamel = hasUDA!(T, UnCamelCaseAttribute); 227 | 228 | foreach(member; __traits(allMembers, T)) { 229 | static if (isWritableDataMember!(T, member)) { 230 | static if (!hasUDA!(__traits(getMember, result, member), NameAttribute)) { 231 | enum pathMember = path ~ member; 232 | static if (unCamel) { 233 | enum pathMemberAlt = path ~ member.unCamelCase; 234 | } 235 | } else { 236 | enum pathMember = path ~ getUDAs!(__traits(getMember, result, member), NameAttribute)[0].name; 237 | static if (unCamel) { 238 | enum pathMemberAlt = pathMember; 239 | } 240 | } 241 | 242 | alias MemberType = typeof(__traits(getMember, result, member)); 243 | 244 | static if (isPointer!MemberType && !isValueType!(PointerTarget!MemberType) || !isValueType!MemberType) { 245 | enum pathNew = pathMember ~ "."; 246 | enum st = Select!(hasUDA!(__traits(getMember, result, member), OptionalAttribute), Strict.no, strict); 247 | static if (isPointer!MemberType) { 248 | if (__traits(getMember, result, member)) 249 | structurize!(st, pathNew)(*__traits(getMember, result, member)); 250 | } else { 251 | structurize!(st, pathNew)(__traits(getMember, result, member)); 252 | } 253 | } else { 254 | enum hash = pathMember.hashOf; 255 | static if (unCamel) { 256 | enum hashAlt = pathMemberAlt.hashOf; 257 | } 258 | 259 | auto index = find_(hash, pathMember); 260 | static if (unCamel && (pathMember != pathMemberAlt)) { 261 | if (!index) 262 | index = find_(hashAlt, pathMemberAlt); 263 | } 264 | 265 | if (index) { 266 | auto pvalue = values_[index - 1]; 267 | 268 | static if ((strict == Strict.no) || (strict == Strict.yesIgnoreNull) || hasUDA!(__traits(getMember, result, member), OptionalAttribute)) { 269 | if (pvalue.isNull) 270 | continue; 271 | } 272 | 273 | __traits(getMember, result, member) = pvalue.get!(Unqual!MemberType); 274 | continue; 275 | } 276 | 277 | static if (((strict == Strict.yes) || (strict == Strict.yesIgnoreNull)) && !hasUDA!(__traits(getMember, result, member), OptionalAttribute)) { 278 | static if (!unCamel || (pathMember == pathMemberAlt)) { 279 | enum ColumnError = format("Column '%s' was not found in this result set", pathMember); 280 | } else { 281 | enum ColumnError = format("Column '%s' or '%s' was not found in this result set", pathMember, pathMemberAlt); 282 | } 283 | throw new MySQLErrorException(ColumnError); 284 | } 285 | } 286 | } 287 | } 288 | } 289 | 290 | MySQLValue[] values_; 291 | const(char)[][] names_; 292 | uint[] index_; 293 | } 294 | 295 | string unCamelCase(string x) { 296 | assert(x.length <= 64); 297 | 298 | enum CharClass { 299 | LowerCase, 300 | UpperCase, 301 | Underscore, 302 | Digit, 303 | } 304 | 305 | CharClass classify(char ch) @nogc @safe pure nothrow { 306 | switch (ch) with (CharClass) { 307 | case 'A':..case 'Z': 308 | return UpperCase; 309 | case 'a':..case 'z': 310 | return LowerCase; 311 | case '0':..case '9': 312 | return Digit; 313 | case '_': 314 | return Underscore; 315 | default: 316 | assert(false, "only supports identifier-type strings"); 317 | } 318 | } 319 | 320 | if (x.length > 0) { 321 | char[128] buffer; 322 | size_t length; 323 | 324 | auto pcls = classify(x.ptr[0]); 325 | foreach (i; 0..x.length) with (CharClass) { 326 | auto ch = x.ptr[i]; 327 | auto cls = classify(ch); 328 | 329 | final switch (cls) { 330 | case Underscore: 331 | buffer[length++] = '_'; 332 | break; 333 | case LowerCase: 334 | buffer[length++] = ch; 335 | break; 336 | case UpperCase: 337 | if ((pcls != UpperCase) && (pcls != Underscore)) 338 | buffer[length++] = '_'; 339 | buffer[length++] = std.ascii.toLower(ch); 340 | break; 341 | case Digit: 342 | if (pcls != Digit) 343 | buffer[length++] = '_'; 344 | buffer[length++] = ch; 345 | break; 346 | } 347 | pcls = cls; 348 | 349 | if (length == buffer.length) 350 | break; 351 | } 352 | return buffer[0..length].idup; 353 | } 354 | return x; 355 | } 356 | 357 | 358 | unittest { 359 | assert("AA".unCamelCase == "aa"); 360 | assert("AaA".unCamelCase == "aa_a"); 361 | assert("AaA1".unCamelCase == "aa_a_1"); 362 | assert("AaA11".unCamelCase == "aa_a_11"); 363 | assert("_AaA1".unCamelCase == "_aa_a_1"); 364 | assert("_AaA11_".unCamelCase == "_aa_a_11_"); 365 | assert("aaA".unCamelCase == "aa_a"); 366 | assert("aaAA".unCamelCase == "aa_aa"); 367 | assert("aaAA1".unCamelCase == "aa_aa_1"); 368 | assert("aaAA11".unCamelCase == "aa_aa_11"); 369 | assert("authorName".unCamelCase == "author_name"); 370 | assert("authorBio".unCamelCase == "author_bio"); 371 | assert("authorPortraitId".unCamelCase == "author_portrait_id"); 372 | assert("authorPortraitID".unCamelCase == "author_portrait_id"); 373 | assert("coverURL".unCamelCase == "cover_url"); 374 | assert("coverImageURL".unCamelCase == "cover_image_url"); 375 | } 376 | -------------------------------------------------------------------------------- /src/mysql/socket.d: -------------------------------------------------------------------------------- 1 | module mysql.socket; 2 | 3 | import vibe.core.net; 4 | import vibe.core.stream; 5 | import vibe.stream.tls; 6 | import vibe.stream.wrapper; 7 | 8 | import mysql.ssl; 9 | 10 | struct VibeSocket { 11 | void connect(const(char)[] host, ushort port) { 12 | socket_ = connectTCP(cast(string)host, port); 13 | socket_.keepAlive = true; 14 | socket_.tcpNoDelay = true; 15 | static if (is(StreamProxy)) { 16 | stream_ = createProxyStream(socket_); 17 | } else { 18 | stream_ = socket_; 19 | } 20 | } 21 | 22 | bool connected() inout { 23 | return socket_ && socket_.connected(); 24 | } 25 | 26 | void close() { 27 | if (socket_) { 28 | socket_.close(); 29 | static if (is(typeof(socket_) == class)) { 30 | socket_ = null; 31 | } 32 | } 33 | } 34 | 35 | void read(ubyte[] buffer) { 36 | stream_.read(buffer); 37 | } 38 | 39 | void write(in ubyte[] buffer) { 40 | stream_.write(buffer); 41 | } 42 | 43 | void flush() { 44 | stream_.flush(); 45 | } 46 | 47 | bool empty() { 48 | return stream_.empty; 49 | } 50 | 51 | void startSSL(const(char)[] hostName, SSLConfig config) { 52 | TLSVersion tlsVersion; 53 | 54 | final switch (config.sslVersion) with (SSLConfig.Version) { 55 | case any: 56 | tlsVersion = TLSVersion.any; 57 | break; 58 | case ssl3: 59 | tlsVersion = TLSVersion.ssl3; 60 | break; 61 | case tls1: 62 | tlsVersion = TLSVersion.tls1; 63 | break; 64 | case tls1_1: 65 | tlsVersion = TLSVersion.tls1_1; 66 | break; 67 | case tls1_2: 68 | tlsVersion = TLSVersion.tls1_2; 69 | break; 70 | case dtls1: 71 | tlsVersion = TLSVersion.dtls1; 72 | break; 73 | } 74 | 75 | TLSPeerValidationMode peerValidationMode; 76 | 77 | final switch (config.validate) with (SSLConfig.Validate) { 78 | case basic: 79 | peerValidationMode = TLSPeerValidationMode.checkCert | TLSPeerValidationMode.requireCert; 80 | break; 81 | case trust: 82 | peerValidationMode = TLSPeerValidationMode.checkCert | TLSPeerValidationMode.requireCert | TLSPeerValidationMode.checkTrust; 83 | break; 84 | case identity: 85 | peerValidationMode = TLSPeerValidationMode.checkCert | TLSPeerValidationMode.requireCert | TLSPeerValidationMode.checkTrust | TLSPeerValidationMode.checkPeer; 86 | break; 87 | } 88 | 89 | auto ctx = createTLSContext(TLSContextKind.client, tlsVersion); 90 | ctx.peerValidationMode = peerValidationMode; 91 | 92 | if (config.rootCertFile.length) 93 | ctx.useTrustedCertificateFile(config.rootCertFile.idup); 94 | 95 | if (config.ciphers.length) 96 | ctx.setCipherList(config.ciphers.idup); 97 | 98 | auto peerName = config.hostName.length ? config.hostName : hostName; 99 | 100 | stream_ = createTLSStream(socket_, ctx, TLSStreamState.connecting, peerName.idup, socket_.remoteAddress); 101 | } 102 | 103 | private: 104 | TCPConnection socket_; 105 | static if (is(StreamProxy)) { 106 | StreamProxy stream_; 107 | } else { 108 | Stream stream_; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/mysql/ssl.d: -------------------------------------------------------------------------------- 1 | module mysql.ssl; 2 | 3 | 4 | static struct SSLConfig { 5 | enum Version { 6 | any = 0, 7 | ssl3, 8 | tls1, 9 | tls1_1, 10 | tls1_2, 11 | dtls1, 12 | } 13 | 14 | enum Validate { 15 | basic = 0, 16 | trust, 17 | identity, 18 | } 19 | 20 | bool enforce; 21 | 22 | Version sslVersion = Version.any; 23 | Validate validate = Validate.basic; 24 | 25 | const(char)[] hostName; 26 | const(char)[] rootCertFile; 27 | const(char)[] ciphers; 28 | } 29 | -------------------------------------------------------------------------------- /src/mysql/type.d: -------------------------------------------------------------------------------- 1 | module mysql.type; 2 | 3 | 4 | import std.algorithm; 5 | import std.array : appender; 6 | import std.conv : parse, to; 7 | import std.datetime; 8 | import std.format: format, formattedWrite; 9 | import std.traits; 10 | import std.typecons; 11 | 12 | import mysql.protocol; 13 | import mysql.packet; 14 | import mysql.exception; 15 | public import mysql.row; 16 | 17 | struct IgnoreAttribute {} 18 | struct OptionalAttribute {} 19 | struct NameAttribute { const(char)[] name; } 20 | struct UnCamelCaseAttribute {} 21 | struct TableNameAttribute {const(char)[] name;} 22 | 23 | @property TableNameAttribute tableName(const(char)[] name) { 24 | return TableNameAttribute(name); 25 | } 26 | 27 | @property IgnoreAttribute ignore() { 28 | return IgnoreAttribute(); 29 | } 30 | 31 | 32 | @property OptionalAttribute optional() { 33 | return OptionalAttribute(); 34 | } 35 | 36 | 37 | @property NameAttribute as(const(char)[] name) { 38 | return NameAttribute(name); 39 | } 40 | 41 | 42 | @property UnCamelCaseAttribute uncamel() { 43 | return UnCamelCaseAttribute(); 44 | } 45 | 46 | 47 | template Unnull(U) { 48 | alias impl(N : Nullable!T, T) = T; 49 | alias impl(T) = T; 50 | alias Unnull = impl!U; 51 | } 52 | 53 | alias Unboth(T) = Unqual!(Unnull!T); 54 | enum isSomeDuration(T) = is(Unboth!T == Date) || is(Unboth!T == DateTime) || is(Unboth!T == SysTime) || is(Unboth!T == Duration) || is(Unboth!T == TimeOfDay); 55 | enum isValueType(T) = isSomeDuration!(Unboth!T) || is(Unboth!T == MySQLValue) || (!is(Unboth!T == struct) && !is(Unboth!T == class)); 56 | 57 | template isWritableDataMember(T, string Member) { 58 | static if (is(TypeTuple!(__traits(getMember, T, Member)))) { 59 | enum isWritableDataMember = false; 60 | } else static if (!is(typeof(__traits(getMember, T, Member)))) { 61 | enum isWritableDataMember = false; 62 | } else static if (is(typeof(__traits(getMember, T, Member)) == void)) { 63 | enum isWritableDataMember = false; 64 | } else static if (is(typeof(__traits(getMember, T, Member)) == enum)) { 65 | enum isWritableDataMember = true; 66 | } else static if (hasUDA!(__traits(getMember, T, Member), IgnoreAttribute)) { 67 | enum isWritableDataMember = false; 68 | } else static if (isArray!(typeof(__traits(getMember, T, Member))) && !is(typeof(typeof(__traits(getMember, T, Member)).init[0]) == ubyte) && !is(typeof(__traits(getMember, T, Member)) == string)) { 69 | enum isWritableDataMember = false; 70 | } else static if (isAssociativeArray!(typeof(__traits(getMember, T, Member)))) { 71 | enum isWritableDataMember = false; 72 | } else static if (isSomeFunction!(typeof(__traits(getMember, T, Member)))) { 73 | enum isWritableDataMember = false; 74 | } else static if (!is(typeof((){ T x = void; __traits(getMember, x, Member) = __traits(getMember, x, Member); }()))) { 75 | enum isWritableDataMember = false; 76 | } else static if ((__traits(getProtection, __traits(getMember, T, Member)) != "public") && (__traits(getProtection, __traits(getMember, T, Member)) != "export")) { 77 | enum isWritableDataMember = false; 78 | } else { 79 | enum isWritableDataMember = true; 80 | } 81 | } 82 | 83 | template isReadableDataMember(T, string Member) { 84 | static if (is(TypeTuple!(__traits(getMember, T, Member)))) { 85 | enum isReadableDataMember = false; 86 | } else static if (!is(typeof(__traits(getMember, T, Member)))) { 87 | enum isReadableDataMember = false; 88 | } else static if (is(typeof(__traits(getMember, T, Member)) == void)) { 89 | enum isReadableDataMember = false; 90 | } else static if (is(typeof(__traits(getMember, T, Member)) == enum)) { 91 | enum isReadableDataMember = true; 92 | } else static if (hasUDA!(__traits(getMember, T, Member), IgnoreAttribute)) { 93 | enum isReadableDataMember = false; 94 | } else static if (isArray!(typeof(__traits(getMember, T, Member))) && !is(typeof(typeof(__traits(getMember, T, Member)).init[0]) == ubyte) && !is(typeof(__traits(getMember, T, Member)) == string)) { 95 | enum isReadableDataMember = false; 96 | } else static if (isAssociativeArray!(typeof(__traits(getMember, T, Member)))) { 97 | enum isReadableDataMember = false; 98 | } else static if (isSomeFunction!(typeof(__traits(getMember, T, Member))) /* && return type is valueType*/ ) { 99 | enum isReadableDataMember = true; 100 | } else static if (!is(typeof((){ T x = void; __traits(getMember, x, Member) = __traits(getMember, x, Member); }()))) { 101 | enum isReadableDataMember = false; 102 | } else static if ((__traits(getProtection, __traits(getMember, T, Member)) != "public") && (__traits(getProtection, __traits(getMember, T, Member)) != "export")) { 103 | enum isReadableDataMember = false; 104 | } else { 105 | enum isReadableDataMember = true; 106 | } 107 | } 108 | 109 | struct MySQLRawString { 110 | @disable this(); 111 | 112 | this(const(char)[] data) { 113 | data_ = data; 114 | } 115 | 116 | @property auto length() const { 117 | return data_.length; 118 | } 119 | 120 | @property auto data() const { 121 | return data_; 122 | } 123 | 124 | private const(char)[] data_; 125 | } 126 | 127 | 128 | 129 | struct MySQLFragment { 130 | @disable this(); 131 | 132 | this(const(char)[] data) { 133 | data_ = data; 134 | } 135 | 136 | @property auto length() const { 137 | return data_.length; 138 | } 139 | 140 | @property auto data() const { 141 | return data_; 142 | } 143 | 144 | private const(char)[] data_; 145 | } 146 | 147 | 148 | struct MySQLBinary { 149 | this(T)(T[] data) { 150 | data_ = (cast(ubyte*)data.ptr)[0..typeof(T[].init[0]).sizeof * data.length]; 151 | } 152 | 153 | @property auto length() const { 154 | return data_.length; 155 | } 156 | 157 | @property auto data() const { 158 | return data_; 159 | } 160 | 161 | private const(ubyte)[] data_; 162 | } 163 | 164 | 165 | struct MySQLValue { 166 | package enum BufferSize = max(ulong.sizeof, (ulong[]).sizeof, MySQLDateTime.sizeof, MySQLTime.sizeof); 167 | package this(const(char)[] name, ColumnTypes type, bool signed, void* ptr, size_t size) { 168 | assert(size <= BufferSize); 169 | type_ = type; 170 | sign_ = signed ? 0x00 : 0x80; 171 | if (type != ColumnTypes.MYSQL_TYPE_NULL) 172 | buffer_[0..size] = (cast(ubyte*)ptr)[0..size]; 173 | name_ = name; 174 | } 175 | 176 | this(T)(T) if (is(Unqual!T == typeof(null))) { 177 | type_ = ColumnTypes.MYSQL_TYPE_NULL; 178 | sign_ = 0x00; 179 | } 180 | 181 | this(T)(T value) if (is(Unqual!T == MySQLValue)) { 182 | this = value; 183 | } 184 | 185 | this(T)(T value) if (std.traits.isFloatingPoint!T) { 186 | alias UT = Unqual!T; 187 | 188 | sign_ = 0x00; 189 | static if (is(UT == float)) { 190 | type_ = ColumnTypes.MYSQL_TYPE_FLOAT; 191 | buffer_[0..T.sizeof] = (cast(ubyte*)&value)[0..T.sizeof]; 192 | } else static if (is(UT == double)) { 193 | type_ = ColumnTypes.MYSQL_TYPE_DOUBLE; 194 | buffer_[0..T.sizeof] = (cast(ubyte*)&value)[0..T.sizeof]; 195 | } else { 196 | type_ = ColumnTypes.MYSQL_TYPE_DOUBLE; 197 | auto data = cast(double)value; 198 | buffer_[0..typeof(data).sizeof] = (cast(ubyte*)&data)[0..typeof(data).sizeof]; 199 | } 200 | } 201 | 202 | this(T)(T value) if (isIntegral!T || isBoolean!T) { 203 | alias UT = Unqual!T; 204 | 205 | static if (is(UT == long) || is(UT == ulong)) { 206 | type_ = ColumnTypes.MYSQL_TYPE_LONGLONG; 207 | } else static if (is(UT == int) || is(UT == uint) || is(UT == dchar)) { 208 | type_ = ColumnTypes.MYSQL_TYPE_LONG; 209 | } else static if (is(UT == short) || is(UT == ushort) || is(UT == wchar)) { 210 | type_ = ColumnTypes.MYSQL_TYPE_SHORT; 211 | } else { 212 | type_ = ColumnTypes.MYSQL_TYPE_TINY; 213 | } 214 | 215 | sign_ = isUnsigned!UT ? 0x80 : 0x00; 216 | buffer_[0..T.sizeof] = (cast(ubyte*)&value)[0..T.sizeof]; 217 | } 218 | 219 | this(T)(T value) if (is(Unqual!T == Date) || is(Unqual!T == DateTime) || is(Unqual!T == SysTime)) { 220 | type_ = ColumnTypes.MYSQL_TYPE_TIMESTAMP; 221 | sign_ = 0x00; 222 | (*cast(MySQLDateTime*)buffer_) = MySQLDateTime.from(value); 223 | } 224 | 225 | this(T)(T value) if (is(Unqual!T == Duration) || is(Unqual!T == TimeOfDay)) { 226 | type_ = ColumnTypes.MYSQL_TYPE_TIME; 227 | sign_ = 0x00; 228 | (*cast(MySQLTime*)buffer_) = MySQLTime.from(value); 229 | } 230 | 231 | this(T)(T value) if (isSomeString!(OriginalType!T)) { 232 | static assert(typeof(T.init[0]).sizeof == 1, format("Unsupported string type: %s", T.stringof)); 233 | 234 | type_ = ColumnTypes.MYSQL_TYPE_STRING; 235 | sign_ = 0x80; 236 | 237 | auto slice = value[0..$]; 238 | buffer_.ptr[0..typeof(slice).sizeof] = (cast(ubyte*)&slice)[0..typeof(slice).sizeof]; 239 | } 240 | 241 | this(T)(T value) if (is(Unqual!T == MySQLBinary)) { 242 | type_ = ColumnTypes.MYSQL_TYPE_BLOB; 243 | sign_ = 0x80; 244 | buffer_.ptr[0..(ubyte[]).sizeof] = (cast(ubyte*)&value.data_)[0..(ubyte[]).sizeof]; 245 | } 246 | 247 | void toString(Appender)(ref Appender app) const { 248 | final switch(type_) with (ColumnTypes) { 249 | case MYSQL_TYPE_NULL: 250 | break; 251 | case MYSQL_TYPE_TINY: 252 | if (isSigned) formattedWrite(&app, "%d", *cast(ubyte*)buffer_.ptr); 253 | else formattedWrite(&app, "%d", *cast(byte*)buffer_.ptr); 254 | break; 255 | case MYSQL_TYPE_YEAR: 256 | case MYSQL_TYPE_SHORT: 257 | if (isSigned) formattedWrite(&app, "%d", *cast(short*)buffer_.ptr); 258 | else formattedWrite(&app, "%d", *cast(ushort*)buffer_.ptr); 259 | break; 260 | case MYSQL_TYPE_INT24: 261 | case MYSQL_TYPE_LONG: 262 | if (isSigned) formattedWrite(&app, "%d", *cast(int*)buffer_.ptr); 263 | else formattedWrite(&app, "%d", *cast(uint*)buffer_.ptr); 264 | break; 265 | case MYSQL_TYPE_LONGLONG: 266 | if (isSigned) formattedWrite(&app, "%d", *cast(long*)buffer_.ptr); 267 | else formattedWrite(&app, "%d", *cast(ulong*)buffer_.ptr); 268 | break; 269 | case MYSQL_TYPE_FLOAT: 270 | formattedWrite(&app, "%g", *cast(float*)buffer_.ptr); 271 | break; 272 | case MYSQL_TYPE_DOUBLE: 273 | formattedWrite(&app, "%g", *cast(double*)buffer_.ptr); 274 | break; 275 | case MYSQL_TYPE_SET: 276 | case MYSQL_TYPE_ENUM: 277 | case MYSQL_TYPE_VARCHAR: 278 | case MYSQL_TYPE_VAR_STRING: 279 | case MYSQL_TYPE_STRING: 280 | case MYSQL_TYPE_JSON: 281 | case MYSQL_TYPE_NEWDECIMAL: 282 | case MYSQL_TYPE_DECIMAL: 283 | case MYSQL_TYPE_TINY_BLOB: 284 | case MYSQL_TYPE_MEDIUM_BLOB: 285 | case MYSQL_TYPE_LONG_BLOB: 286 | case MYSQL_TYPE_BLOB: 287 | app.put(*cast(string*)buffer_.ptr); 288 | break; 289 | case MYSQL_TYPE_BIT: 290 | case MYSQL_TYPE_GEOMETRY: 291 | formattedWrite(&app, "%s", *cast(ubyte[]*)buffer_.ptr); 292 | break; 293 | case MYSQL_TYPE_TIME: 294 | case MYSQL_TYPE_TIME2: 295 | formattedWrite(&app, "%s", (*cast(MySQLTime*)buffer_.ptr).to!Duration()); 296 | break; 297 | case MYSQL_TYPE_DATE: 298 | case MYSQL_TYPE_NEWDATE: 299 | case MYSQL_TYPE_DATETIME: 300 | case MYSQL_TYPE_DATETIME2: 301 | case MYSQL_TYPE_TIMESTAMP: 302 | case MYSQL_TYPE_TIMESTAMP2: 303 | formattedWrite(&app, "%s", (*cast(MySQLDateTime*)buffer_.ptr).to!DateTime()); 304 | break; 305 | } 306 | } 307 | 308 | string toString() const { 309 | auto app = appender!string; 310 | toString(app); 311 | return app.data; 312 | } 313 | 314 | bool opEquals(MySQLValue other) const { 315 | if (isString && other.isString) { 316 | return peek!string == other.peek!string; 317 | } else if (isScalar && other.isScalar) { 318 | if (isFloatingPoint || other.isFloatingPoint) 319 | return get!double == other.get!double; 320 | if (isSigned || other.isSigned) 321 | return get!long == other.get!long; 322 | return get!ulong == other.get!ulong; 323 | } else if (isTime && other.isTime) { 324 | return get!Duration == other.get!Duration; 325 | } else if (isTimestamp && other.isTimestamp) { 326 | return get!SysTime == other.get!SysTime; 327 | } else if (isNull && other.isNull) { 328 | return true; 329 | } 330 | return false; 331 | } 332 | 333 | T get(T)(lazy T def) const { 334 | return !isNull ? get!T : def; 335 | } 336 | 337 | T get(T)() const if (isScalarType!T && !is(T == enum)) { 338 | switch(type_) with (ColumnTypes) { 339 | case MYSQL_TYPE_TINY: 340 | return cast(T)(*cast(ubyte*)buffer_.ptr); 341 | case MYSQL_TYPE_YEAR: 342 | case MYSQL_TYPE_SHORT: 343 | return cast(T)(*cast(ushort*)buffer_.ptr); 344 | case MYSQL_TYPE_INT24: 345 | case MYSQL_TYPE_LONG: 346 | return cast(T)(*cast(uint*)buffer_.ptr); 347 | case MYSQL_TYPE_LONGLONG: 348 | return cast(T)(*cast(ulong*)buffer_.ptr); 349 | case MYSQL_TYPE_FLOAT: 350 | return cast(T)(*cast(float*)buffer_.ptr); 351 | case MYSQL_TYPE_DOUBLE: 352 | return cast(T)(*cast(double*)buffer_.ptr); 353 | default: 354 | throw new MySQLErrorException(format("Cannot convert '%s' from %s to %s", name_, columnTypeName(type_), T.stringof)); 355 | } 356 | } 357 | 358 | T get(T)() const if (is(Unqual!T == SysTime) || is(Unqual!T == DateTime) || is(Unqual!T == Date)) { 359 | switch(type_) with (ColumnTypes) { 360 | case MYSQL_TYPE_DATE: 361 | case MYSQL_TYPE_NEWDATE: 362 | case MYSQL_TYPE_DATETIME: 363 | case MYSQL_TYPE_DATETIME2: 364 | case MYSQL_TYPE_TIMESTAMP: 365 | case MYSQL_TYPE_TIMESTAMP2: 366 | return (*cast(MySQLDateTime*)buffer_.ptr).to!T; 367 | default: 368 | throw new MySQLErrorException(format("Cannot convert '%s' from %s to %s", name_, columnTypeName(type_), T.stringof)); 369 | } 370 | } 371 | 372 | T get(T)() const if (is(Unqual!T == TimeOfDay)) { 373 | switch(type_) with (ColumnTypes) { 374 | case MYSQL_TYPE_DATE: 375 | case MYSQL_TYPE_NEWDATE: 376 | case MYSQL_TYPE_DATETIME: 377 | case MYSQL_TYPE_DATETIME2: 378 | case MYSQL_TYPE_TIMESTAMP: 379 | case MYSQL_TYPE_TIMESTAMP2: 380 | return (*cast(MySQLDateTime*)buffer_.ptr).to!T; 381 | case MYSQL_TYPE_TIME: 382 | case MYSQL_TYPE_TIME2: 383 | return (*cast(MySQLTime*)buffer_.ptr).to!T; 384 | default: 385 | throw new MySQLErrorException(format("Cannot convert '%s' from %s to %s", name_, columnTypeName(type_), T.stringof)); 386 | } 387 | } 388 | 389 | T get(T)() const if (is(Unqual!T == Duration)) { 390 | switch(type_) with (ColumnTypes) { 391 | case MYSQL_TYPE_TIME: 392 | case MYSQL_TYPE_TIME2: 393 | return (*cast(MySQLTime*)buffer_.ptr).to!T; 394 | default: 395 | throw new MySQLErrorException(format("Cannot convert '%s' from %s to %s", name_, columnTypeName(type_), T.stringof)); 396 | } 397 | } 398 | 399 | T get(T)() const if (is(Unqual!T == enum)) { 400 | return cast(T)get!(OriginalType!T); 401 | } 402 | 403 | T get(T)() const if (isArray!T && !is(T == enum)) { 404 | switch(type_) with (ColumnTypes) { 405 | case MYSQL_TYPE_SET: 406 | case MYSQL_TYPE_ENUM: 407 | case MYSQL_TYPE_VARCHAR: 408 | case MYSQL_TYPE_VAR_STRING: 409 | case MYSQL_TYPE_STRING: 410 | case MYSQL_TYPE_JSON: 411 | case MYSQL_TYPE_NEWDECIMAL: 412 | case MYSQL_TYPE_DECIMAL: 413 | return (*cast(T*)buffer_.ptr).dup; 414 | case MYSQL_TYPE_BIT: 415 | case MYSQL_TYPE_TINY_BLOB: 416 | case MYSQL_TYPE_MEDIUM_BLOB: 417 | case MYSQL_TYPE_LONG_BLOB: 418 | case MYSQL_TYPE_BLOB: 419 | case MYSQL_TYPE_GEOMETRY: 420 | return (*cast(T*)buffer_.ptr).dup; 421 | default: 422 | throw new MySQLErrorException(format("Cannot convert '%s' from %s to %s", name_, columnTypeName(type_), T.stringof)); 423 | } 424 | } 425 | 426 | T get(T)() const if(isInstanceOf!(Nullable, T)) { 427 | if (type_ == ColumnTypes.MYSQL_TYPE_NULL) 428 | return T.init; 429 | return T(get!(typeof(T.init.get))); 430 | } 431 | 432 | T peek(T)(lazy T def) const { 433 | return !isNull ? peek!(T) : def; 434 | } 435 | 436 | T peek(T)() const if (isScalarType!T) { 437 | return get!(T); 438 | } 439 | 440 | T peek(T)() const if (is(Unqual!T == SysTime) || is(Unqual!T == DateTime) || is(Unqual!T == Date) || is(Unqual!T == TimeOfDay)) { 441 | return get!(T); 442 | } 443 | 444 | T peek(T)() const if (is(Unqual!T == Duration)) { 445 | return get!(T); 446 | } 447 | 448 | T peek(T)() const if (isArray!T) { 449 | switch(type_) with (ColumnTypes) { 450 | case MYSQL_TYPE_SET: 451 | case MYSQL_TYPE_ENUM: 452 | case MYSQL_TYPE_VARCHAR: 453 | case MYSQL_TYPE_VAR_STRING: 454 | case MYSQL_TYPE_STRING: 455 | case MYSQL_TYPE_JSON: 456 | case MYSQL_TYPE_NEWDECIMAL: 457 | case MYSQL_TYPE_DECIMAL: 458 | return (*cast(T*)buffer_.ptr); 459 | case MYSQL_TYPE_BIT: 460 | case MYSQL_TYPE_TINY_BLOB: 461 | case MYSQL_TYPE_MEDIUM_BLOB: 462 | case MYSQL_TYPE_LONG_BLOB: 463 | case MYSQL_TYPE_BLOB: 464 | case MYSQL_TYPE_GEOMETRY: 465 | return (*cast(T*)buffer_.ptr); 466 | default: 467 | throw new MySQLErrorException(format("Cannot convert '%s' from %s to %s", name_, columnTypeName(type_), T.stringof)); 468 | } 469 | } 470 | 471 | bool isNull() const { 472 | return type_ == ColumnTypes.MYSQL_TYPE_NULL; 473 | } 474 | 475 | ColumnTypes type() const { 476 | return type_; 477 | } 478 | 479 | bool isSigned() const { 480 | return sign_ == 0x00; 481 | } 482 | 483 | bool isString() const { 484 | final switch(type_) with (ColumnTypes) { 485 | case MYSQL_TYPE_NULL: 486 | return false; 487 | case MYSQL_TYPE_TINY: 488 | case MYSQL_TYPE_YEAR: 489 | case MYSQL_TYPE_SHORT: 490 | case MYSQL_TYPE_INT24: 491 | case MYSQL_TYPE_LONG: 492 | case MYSQL_TYPE_LONGLONG: 493 | case MYSQL_TYPE_FLOAT: 494 | case MYSQL_TYPE_DOUBLE: 495 | return false; 496 | case MYSQL_TYPE_SET: 497 | case MYSQL_TYPE_ENUM: 498 | case MYSQL_TYPE_VARCHAR: 499 | case MYSQL_TYPE_VAR_STRING: 500 | case MYSQL_TYPE_STRING: 501 | case MYSQL_TYPE_JSON: 502 | case MYSQL_TYPE_NEWDECIMAL: 503 | case MYSQL_TYPE_DECIMAL: 504 | case MYSQL_TYPE_TINY_BLOB: 505 | case MYSQL_TYPE_MEDIUM_BLOB: 506 | case MYSQL_TYPE_LONG_BLOB: 507 | case MYSQL_TYPE_BLOB: 508 | return true; 509 | case MYSQL_TYPE_BIT: 510 | case MYSQL_TYPE_GEOMETRY: 511 | return false; 512 | case MYSQL_TYPE_TIME: 513 | case MYSQL_TYPE_TIME2: 514 | return false; 515 | case MYSQL_TYPE_DATE: 516 | case MYSQL_TYPE_NEWDATE: 517 | case MYSQL_TYPE_DATETIME: 518 | case MYSQL_TYPE_DATETIME2: 519 | case MYSQL_TYPE_TIMESTAMP: 520 | case MYSQL_TYPE_TIMESTAMP2: 521 | return false; 522 | } 523 | } 524 | 525 | bool isScalar() const { 526 | final switch(type_) with (ColumnTypes) { 527 | case MYSQL_TYPE_NULL: 528 | return false; 529 | case MYSQL_TYPE_TINY: 530 | case MYSQL_TYPE_YEAR: 531 | case MYSQL_TYPE_SHORT: 532 | case MYSQL_TYPE_INT24: 533 | case MYSQL_TYPE_LONG: 534 | case MYSQL_TYPE_LONGLONG: 535 | case MYSQL_TYPE_FLOAT: 536 | case MYSQL_TYPE_DOUBLE: 537 | return true; 538 | case MYSQL_TYPE_SET: 539 | case MYSQL_TYPE_ENUM: 540 | case MYSQL_TYPE_VARCHAR: 541 | case MYSQL_TYPE_VAR_STRING: 542 | case MYSQL_TYPE_STRING: 543 | case MYSQL_TYPE_JSON: 544 | case MYSQL_TYPE_NEWDECIMAL: 545 | case MYSQL_TYPE_DECIMAL: 546 | case MYSQL_TYPE_TINY_BLOB: 547 | case MYSQL_TYPE_MEDIUM_BLOB: 548 | case MYSQL_TYPE_LONG_BLOB: 549 | case MYSQL_TYPE_BLOB: 550 | return false; 551 | case MYSQL_TYPE_BIT: 552 | case MYSQL_TYPE_GEOMETRY: 553 | return false; 554 | case MYSQL_TYPE_TIME: 555 | case MYSQL_TYPE_TIME2: 556 | return false; 557 | case MYSQL_TYPE_DATE: 558 | case MYSQL_TYPE_NEWDATE: 559 | case MYSQL_TYPE_DATETIME: 560 | case MYSQL_TYPE_DATETIME2: 561 | case MYSQL_TYPE_TIMESTAMP: 562 | case MYSQL_TYPE_TIMESTAMP2: 563 | return false; 564 | } 565 | } 566 | 567 | bool isFloatingPoint() const { 568 | final switch(type_) with (ColumnTypes) { 569 | case MYSQL_TYPE_NULL: 570 | return false; 571 | case MYSQL_TYPE_TINY: 572 | case MYSQL_TYPE_YEAR: 573 | case MYSQL_TYPE_SHORT: 574 | case MYSQL_TYPE_INT24: 575 | case MYSQL_TYPE_LONG: 576 | case MYSQL_TYPE_LONGLONG: 577 | return false; 578 | case MYSQL_TYPE_FLOAT: 579 | case MYSQL_TYPE_DOUBLE: 580 | return true; 581 | case MYSQL_TYPE_SET: 582 | case MYSQL_TYPE_ENUM: 583 | case MYSQL_TYPE_VARCHAR: 584 | case MYSQL_TYPE_VAR_STRING: 585 | case MYSQL_TYPE_STRING: 586 | case MYSQL_TYPE_JSON: 587 | case MYSQL_TYPE_NEWDECIMAL: 588 | case MYSQL_TYPE_DECIMAL: 589 | case MYSQL_TYPE_TINY_BLOB: 590 | case MYSQL_TYPE_MEDIUM_BLOB: 591 | case MYSQL_TYPE_LONG_BLOB: 592 | case MYSQL_TYPE_BLOB: 593 | return false; 594 | case MYSQL_TYPE_BIT: 595 | case MYSQL_TYPE_GEOMETRY: 596 | return false; 597 | case MYSQL_TYPE_TIME: 598 | case MYSQL_TYPE_TIME2: 599 | return false; 600 | case MYSQL_TYPE_DATE: 601 | case MYSQL_TYPE_NEWDATE: 602 | case MYSQL_TYPE_DATETIME: 603 | case MYSQL_TYPE_DATETIME2: 604 | case MYSQL_TYPE_TIMESTAMP: 605 | case MYSQL_TYPE_TIMESTAMP2: 606 | return false; 607 | } 608 | } 609 | 610 | bool isTime() const { 611 | final switch(type_) with (ColumnTypes) { 612 | case MYSQL_TYPE_NULL: 613 | return false; 614 | case MYSQL_TYPE_TINY: 615 | case MYSQL_TYPE_YEAR: 616 | case MYSQL_TYPE_SHORT: 617 | case MYSQL_TYPE_INT24: 618 | case MYSQL_TYPE_LONG: 619 | case MYSQL_TYPE_LONGLONG: 620 | case MYSQL_TYPE_FLOAT: 621 | case MYSQL_TYPE_DOUBLE: 622 | return false; 623 | case MYSQL_TYPE_SET: 624 | case MYSQL_TYPE_ENUM: 625 | case MYSQL_TYPE_VARCHAR: 626 | case MYSQL_TYPE_VAR_STRING: 627 | case MYSQL_TYPE_STRING: 628 | case MYSQL_TYPE_JSON: 629 | case MYSQL_TYPE_NEWDECIMAL: 630 | case MYSQL_TYPE_DECIMAL: 631 | case MYSQL_TYPE_TINY_BLOB: 632 | case MYSQL_TYPE_MEDIUM_BLOB: 633 | case MYSQL_TYPE_LONG_BLOB: 634 | case MYSQL_TYPE_BLOB: 635 | return false; 636 | case MYSQL_TYPE_BIT: 637 | case MYSQL_TYPE_GEOMETRY: 638 | return false; 639 | case MYSQL_TYPE_TIME: 640 | case MYSQL_TYPE_TIME2: 641 | return true; 642 | case MYSQL_TYPE_DATE: 643 | case MYSQL_TYPE_NEWDATE: 644 | case MYSQL_TYPE_DATETIME: 645 | case MYSQL_TYPE_DATETIME2: 646 | case MYSQL_TYPE_TIMESTAMP: 647 | case MYSQL_TYPE_TIMESTAMP2: 648 | return false; 649 | } 650 | } 651 | 652 | alias isDuration = isTime; 653 | 654 | bool isDateTime() const { 655 | final switch(type_) with (ColumnTypes) { 656 | case MYSQL_TYPE_NULL: 657 | return false; 658 | case MYSQL_TYPE_TINY: 659 | case MYSQL_TYPE_YEAR: 660 | case MYSQL_TYPE_SHORT: 661 | case MYSQL_TYPE_INT24: 662 | case MYSQL_TYPE_LONG: 663 | case MYSQL_TYPE_LONGLONG: 664 | case MYSQL_TYPE_FLOAT: 665 | case MYSQL_TYPE_DOUBLE: 666 | return false; 667 | case MYSQL_TYPE_SET: 668 | case MYSQL_TYPE_ENUM: 669 | case MYSQL_TYPE_VARCHAR: 670 | case MYSQL_TYPE_VAR_STRING: 671 | case MYSQL_TYPE_STRING: 672 | case MYSQL_TYPE_JSON: 673 | case MYSQL_TYPE_NEWDECIMAL: 674 | case MYSQL_TYPE_DECIMAL: 675 | case MYSQL_TYPE_TINY_BLOB: 676 | case MYSQL_TYPE_MEDIUM_BLOB: 677 | case MYSQL_TYPE_LONG_BLOB: 678 | case MYSQL_TYPE_BLOB: 679 | return false; 680 | case MYSQL_TYPE_BIT: 681 | case MYSQL_TYPE_GEOMETRY: 682 | return false; 683 | case MYSQL_TYPE_TIME: 684 | case MYSQL_TYPE_TIME2: 685 | return false; 686 | case MYSQL_TYPE_DATE: 687 | case MYSQL_TYPE_NEWDATE: 688 | case MYSQL_TYPE_DATETIME: 689 | case MYSQL_TYPE_DATETIME2: 690 | case MYSQL_TYPE_TIMESTAMP: 691 | case MYSQL_TYPE_TIMESTAMP2: 692 | return true; 693 | } 694 | } 695 | 696 | alias isTimestamp = isDateTime; 697 | 698 | private: 699 | ColumnTypes type_ = ColumnTypes.MYSQL_TYPE_NULL; 700 | ubyte sign_; 701 | ubyte[6] pad_; 702 | ubyte[BufferSize] buffer_; 703 | const(char)[] name_; 704 | } 705 | 706 | 707 | struct MySQLColumn { 708 | uint length; 709 | ushort flags; 710 | ubyte decimals; 711 | ColumnTypes type; 712 | const(char)[] name; 713 | } 714 | 715 | 716 | alias MySQLHeader = MySQLColumn[]; 717 | 718 | 719 | struct MySQLTime { 720 | uint days; 721 | ubyte negative; 722 | ubyte hours; 723 | ubyte mins; 724 | ubyte secs; 725 | uint usecs; 726 | 727 | auto to(T)() const if (is(Unqual!T == Duration)) { 728 | auto total = days * 86400_000_000L + 729 | hours * 3600_000_000L + 730 | mins * 60_000_000L + 731 | secs * 1_000_000L + 732 | usecs; 733 | return cast(T)dur!"usecs"(negative ? -total : total); 734 | } 735 | 736 | auto to(T)() const if (is(Unqual!T == TimeOfDay)) { 737 | return cast(T)TimeOfDay(hours, mins, secs); 738 | } 739 | 740 | static MySQLTime from(Duration duration) { 741 | MySQLTime time; 742 | duration.abs.split!("days", "hours", "minutes", "seconds", "usecs")(time.days, time.hours, time.mins, time.secs, time.usecs); 743 | time.negative = duration.isNegative ? 1 : 0; 744 | return time; 745 | } 746 | 747 | static MySQLTime from(TimeOfDay tod) { 748 | MySQLTime time; 749 | time.hours = tod.hour; 750 | time.mins = tod.minute; 751 | time.secs = tod.second; 752 | return time; 753 | } 754 | } 755 | 756 | void putMySQLTime(ref OutputPacket packet, in MySQLTime time) { 757 | if (time.days || time.hours || time.mins || time.mins || time.usecs) { 758 | auto usecs = time.usecs != 0; 759 | packet.put!ubyte(usecs ? 12 : 8); 760 | packet.put!ubyte(time.negative); 761 | packet.put!uint(time.days); 762 | packet.put!ubyte(time.hours); 763 | packet.put!ubyte(time.mins); 764 | packet.put!ubyte(time.secs); 765 | if (usecs) 766 | packet.put!uint(time.usecs); 767 | } else { 768 | packet.put!ubyte(0); 769 | } 770 | } 771 | 772 | auto eatMySQLTime(ref InputPacket packet) { 773 | MySQLTime time; 774 | switch(packet.eat!ubyte) { 775 | case 12: 776 | time.negative = packet.eat!ubyte; 777 | time.days = packet.eat!uint; 778 | time.hours = packet.eat!ubyte; 779 | time.mins = packet.eat!ubyte; 780 | time.secs = packet.eat!ubyte; 781 | time.usecs = packet.eat!uint; 782 | break; 783 | case 8: 784 | time.negative = packet.eat!ubyte; 785 | time.days = packet.eat!uint; 786 | time.hours = packet.eat!ubyte; 787 | time.mins = packet.eat!ubyte; 788 | time.secs = packet.eat!ubyte; 789 | break; 790 | case 0: 791 | break; 792 | default: 793 | throw new MySQLProtocolException("Bad time struct format"); 794 | } 795 | 796 | return time; 797 | } 798 | 799 | 800 | struct MySQLDateTime { 801 | ushort year; 802 | ubyte month; 803 | ubyte day; 804 | ubyte hour; 805 | ubyte min; 806 | ubyte sec; 807 | uint usec; 808 | 809 | bool valid() const { 810 | return month != 0; 811 | } 812 | 813 | T to(T)() const if (is(Unqual!T == SysTime)) { 814 | assert(valid()); 815 | return cast(T)SysTime(DateTime(year, month, day, hour, min, sec), usec.dur!"usecs", UTC()); 816 | } 817 | 818 | T to(T)() const if (is(Unqual!T == DateTime)) { 819 | assert(valid()); 820 | return cast(T)DateTime(year, month, day, hour, min, sec); 821 | } 822 | 823 | T to(T)() const if (is(T == Date)) { 824 | assert(valid()); 825 | return cast(T)Date(year, month, day); 826 | } 827 | 828 | T to(T)() const if (is(Unqual!T == TimeOfDay)) { 829 | return cast(T)TimeOfDay(hour, min, sec); 830 | } 831 | 832 | static MySQLDateTime from(SysTime sysTime) { 833 | MySQLDateTime time; 834 | 835 | auto dateTime = cast(DateTime)sysTime; 836 | time.year = dateTime.year; 837 | time.month = dateTime.month; 838 | time.day = dateTime.day; 839 | time.hour = dateTime.hour; 840 | time.min = dateTime.minute; 841 | time.sec = dateTime.second; 842 | time.usec = cast(int)sysTime.fracSecs.total!"usecs"; 843 | 844 | return time; 845 | } 846 | 847 | static MySQLDateTime from(DateTime dateTime) { 848 | MySQLDateTime time; 849 | 850 | time.year = dateTime.year; 851 | time.month = dateTime.month; 852 | time.day = dateTime.day; 853 | time.hour = dateTime.hour; 854 | time.min = dateTime.minute; 855 | time.sec = dateTime.second; 856 | 857 | return time; 858 | } 859 | 860 | static MySQLDateTime from(Date date) { 861 | MySQLDateTime time; 862 | 863 | time.year = date.year; 864 | time.month = date.month; 865 | time.day = date.day; 866 | 867 | return time; 868 | } 869 | } 870 | 871 | void putMySQLDateTime(ref OutputPacket packet, in MySQLDateTime time) { 872 | auto marker = packet.marker!ubyte; 873 | ubyte length; 874 | 875 | if (time.year || time.month || time.day) { 876 | length = 4; 877 | packet.put!ushort(time.year); 878 | packet.put!ubyte(time.month); 879 | packet.put!ubyte(time.day); 880 | 881 | if (time.hour || time.min || time.sec || time.usec) { 882 | length = 7; 883 | packet.put!ubyte(time.hour); 884 | packet.put!ubyte(time.min); 885 | packet.put!ubyte(time.sec); 886 | 887 | if (time.usec) { 888 | length = 11; 889 | packet.put!uint(time.usec); 890 | } 891 | } 892 | } 893 | 894 | packet.put!ubyte(marker, length); 895 | } 896 | 897 | auto eatMySQLDateTime(ref InputPacket packet) { 898 | MySQLDateTime time; 899 | switch(packet.eat!ubyte) { 900 | case 11: 901 | time.year = packet.eat!ushort; 902 | time.month = packet.eat!ubyte; 903 | time.day = packet.eat!ubyte; 904 | time.hour = packet.eat!ubyte; 905 | time.min = packet.eat!ubyte; 906 | time.sec = packet.eat!ubyte; 907 | time.usec = packet.eat!uint; 908 | break; 909 | case 7: 910 | time.year = packet.eat!ushort; 911 | time.month = packet.eat!ubyte; 912 | time.day = packet.eat!ubyte; 913 | time.hour = packet.eat!ubyte; 914 | time.min = packet.eat!ubyte; 915 | time.sec = packet.eat!ubyte; 916 | break; 917 | case 4: 918 | time.year = packet.eat!ushort; 919 | time.month = packet.eat!ubyte; 920 | time.day = packet.eat!ubyte; 921 | break; 922 | case 0: 923 | break; 924 | default: 925 | throw new MySQLProtocolException("Bad datetime struct format"); 926 | } 927 | 928 | return time; 929 | } 930 | 931 | private void skip(ref const(char)[] x, char ch) { 932 | if (x.length && (x.ptr[0] == ch)) { 933 | x = x[1..$]; 934 | } else { 935 | throw new MySQLProtocolException("Bad datetime string format"); 936 | } 937 | } 938 | 939 | auto parseMySQLTime(const(char)[] x) { 940 | MySQLTime time; 941 | 942 | auto hours = x.parse!int; 943 | if (hours < 0) { 944 | time.negative = 1; 945 | hours = -hours; 946 | } 947 | time.days = hours / 24; 948 | time.hours = cast(ubyte)(hours % 24); 949 | x.skip(':'); 950 | time.mins = x.parse!ubyte; 951 | x.skip(':'); 952 | time.secs = x.parse!ubyte; 953 | if (x.length) { 954 | x.skip('.'); 955 | time.usecs = x.parse!uint; 956 | switch (6 - max(6, x.length)) { 957 | case 0: break; 958 | case 1: time.usecs *= 10; break; 959 | case 2: time.usecs *= 100; break; 960 | case 3: time.usecs *= 1_000; break; 961 | case 4: time.usecs *= 10_000; break; 962 | case 5: time.usecs *= 100_000; break; 963 | default: assert("Bad datetime string format"); break; 964 | } 965 | } 966 | 967 | return time; 968 | } 969 | 970 | auto parseMySQLDateTime(const(char)[] x) { 971 | MySQLDateTime time; 972 | 973 | time.year = x.parse!ushort; 974 | x.skip('-'); 975 | time.month = x.parse!ubyte; 976 | x.skip('-'); 977 | time.day = x.parse!ubyte; 978 | if (x.length) { 979 | x.skip(' '); 980 | time.hour = x.parse!ubyte; 981 | x.skip(':'); 982 | time.min = x.parse!ubyte; 983 | x.skip(':'); 984 | time.sec = x.parse!ubyte; 985 | 986 | if (x.length) { 987 | x.skip('.'); 988 | time.usec = x.parse!uint; 989 | switch (6 - max(6, x.length)) { 990 | case 0: break; 991 | case 1: time.usec *= 10; break; 992 | case 2: time.usec *= 100; break; 993 | case 3: time.usec *= 1_000; break; 994 | case 4: time.usec *= 10_000; break; 995 | case 5: time.usec *= 100_000; break; 996 | default: assert("Bad datetime string format"); break; 997 | } 998 | } 999 | } 1000 | 1001 | return time; 1002 | } 1003 | 1004 | void eatValue(ref InputPacket packet, ref const MySQLColumn column, ref MySQLValue value) { 1005 | auto signed = (column.flags & FieldFlags.UNSIGNED_FLAG) == 0; 1006 | final switch(column.type) with (ColumnTypes) { 1007 | case MYSQL_TYPE_NULL: 1008 | value = MySQLValue(column.name, column.type, signed, null, 0); 1009 | break; 1010 | case MYSQL_TYPE_TINY: 1011 | auto x = packet.eat!ubyte; 1012 | value = MySQLValue(column.name, column.type, signed, &x, 1); 1013 | break; 1014 | case MYSQL_TYPE_YEAR: 1015 | case MYSQL_TYPE_SHORT: 1016 | auto x = packet.eat!ushort; 1017 | value = MySQLValue(column.name, column.type, signed, &x, 2); 1018 | break; 1019 | case MYSQL_TYPE_INT24: 1020 | case MYSQL_TYPE_LONG: 1021 | auto x = packet.eat!uint; 1022 | value = MySQLValue(column.name, column.type, signed, &x, 4); 1023 | break; 1024 | case MYSQL_TYPE_DOUBLE: 1025 | case MYSQL_TYPE_LONGLONG: 1026 | auto x = packet.eat!ulong; 1027 | value = MySQLValue(column.name, column.type, signed, &x, 8); 1028 | break; 1029 | case MYSQL_TYPE_FLOAT: 1030 | auto x = packet.eat!float; 1031 | value = MySQLValue(column.name, column.type, signed, &x, 4); 1032 | break; 1033 | case MYSQL_TYPE_SET: 1034 | case MYSQL_TYPE_ENUM: 1035 | case MYSQL_TYPE_VARCHAR: 1036 | case MYSQL_TYPE_VAR_STRING: 1037 | case MYSQL_TYPE_STRING: 1038 | case MYSQL_TYPE_JSON: 1039 | case MYSQL_TYPE_NEWDECIMAL: 1040 | case MYSQL_TYPE_DECIMAL: 1041 | auto x = packet.eat!(const(char)[])(cast(size_t)packet.eatLenEnc()); 1042 | value = MySQLValue(column.name, column.type, signed, &x, typeof(x).sizeof); 1043 | break; 1044 | case MYSQL_TYPE_BIT: 1045 | case MYSQL_TYPE_TINY_BLOB: 1046 | case MYSQL_TYPE_MEDIUM_BLOB: 1047 | case MYSQL_TYPE_LONG_BLOB: 1048 | case MYSQL_TYPE_BLOB: 1049 | case MYSQL_TYPE_GEOMETRY: 1050 | auto x = packet.eat!(const(ubyte)[])(cast(size_t)packet.eatLenEnc()); 1051 | value = MySQLValue(column.name, column.type, signed, &x, typeof(x).sizeof); 1052 | break; 1053 | case MYSQL_TYPE_TIME: 1054 | case MYSQL_TYPE_TIME2: 1055 | auto x = eatMySQLTime(packet); 1056 | value = MySQLValue(column.name, column.type, signed, &x, typeof(x).sizeof); 1057 | break; 1058 | case MYSQL_TYPE_DATE: 1059 | case MYSQL_TYPE_NEWDATE: 1060 | case MYSQL_TYPE_DATETIME: 1061 | case MYSQL_TYPE_DATETIME2: 1062 | case MYSQL_TYPE_TIMESTAMP: 1063 | case MYSQL_TYPE_TIMESTAMP2: 1064 | auto x = eatMySQLDateTime(packet); 1065 | value = x.valid() ? MySQLValue(column.name, column.type, signed, &x, typeof(x).sizeof) : MySQLValue(column.name, ColumnTypes.MYSQL_TYPE_NULL, signed, null, 0); 1066 | break; 1067 | } 1068 | } 1069 | 1070 | void eatValueText(ref InputPacket packet, ref const MySQLColumn column, ref MySQLValue value) { 1071 | auto signed = (column.flags & FieldFlags.UNSIGNED_FLAG) == 0; 1072 | auto svalue = (column.type != ColumnTypes.MYSQL_TYPE_NULL) ? cast(string)(packet.eat!(const(char)[])(cast(size_t)packet.eatLenEnc())) : string.init; 1073 | final switch(column.type) with (ColumnTypes) { 1074 | case MYSQL_TYPE_NULL: 1075 | value = MySQLValue(column.name, column.type, signed, null, 0); 1076 | break; 1077 | case MYSQL_TYPE_TINY: 1078 | auto x = (svalue.ptr[0] == '-') ? cast(ubyte)(-svalue[1..$].to!byte) : svalue.to!ubyte; 1079 | value = MySQLValue(column.name, column.type, signed, &x, 1); 1080 | break; 1081 | case MYSQL_TYPE_YEAR: 1082 | case MYSQL_TYPE_SHORT: 1083 | auto x = (svalue.ptr[0] == '-') ? cast(ushort)(-svalue[1..$].to!short) : svalue.to!ushort; 1084 | value = MySQLValue(column.name, column.type, signed, &x, 2); 1085 | break; 1086 | case MYSQL_TYPE_INT24: 1087 | case MYSQL_TYPE_LONG: 1088 | auto x = (svalue.ptr[0] == '-') ? cast(uint)(-svalue[1..$].to!int) : svalue.to!uint; 1089 | value = MySQLValue(column.name, column.type, signed, &x, 4); 1090 | break; 1091 | case MYSQL_TYPE_LONGLONG: 1092 | auto x = (svalue.ptr[0] == '-') ? cast(ulong)(-svalue[1..$].to!long) : svalue.to!ulong; 1093 | value = MySQLValue(column.name, column.type, signed, &x, 8); 1094 | break; 1095 | case MYSQL_TYPE_DOUBLE: 1096 | auto x = svalue.to!double; 1097 | value = MySQLValue(column.name, column.type, signed, &x, 8); 1098 | break; 1099 | case MYSQL_TYPE_FLOAT: 1100 | auto x = svalue.to!float; 1101 | value = MySQLValue(column.name, column.type, signed, &x, 4); 1102 | break; 1103 | case MYSQL_TYPE_SET: 1104 | case MYSQL_TYPE_ENUM: 1105 | case MYSQL_TYPE_VARCHAR: 1106 | case MYSQL_TYPE_VAR_STRING: 1107 | case MYSQL_TYPE_STRING: 1108 | case MYSQL_TYPE_JSON: 1109 | case MYSQL_TYPE_NEWDECIMAL: 1110 | case MYSQL_TYPE_DECIMAL: 1111 | value = MySQLValue(column.name, column.type, signed, &svalue, typeof(svalue).sizeof); 1112 | break; 1113 | case MYSQL_TYPE_BIT: 1114 | case MYSQL_TYPE_TINY_BLOB: 1115 | case MYSQL_TYPE_MEDIUM_BLOB: 1116 | case MYSQL_TYPE_LONG_BLOB: 1117 | case MYSQL_TYPE_BLOB: 1118 | case MYSQL_TYPE_GEOMETRY: 1119 | value = MySQLValue(column.name, column.type, signed, &svalue, typeof(svalue).sizeof); 1120 | break; 1121 | case MYSQL_TYPE_TIME: 1122 | case MYSQL_TYPE_TIME2: 1123 | auto x = parseMySQLTime(svalue); 1124 | value = MySQLValue(column.name, column.type, signed, &x, typeof(x).sizeof); 1125 | break; 1126 | case MYSQL_TYPE_DATE: 1127 | case MYSQL_TYPE_NEWDATE: 1128 | case MYSQL_TYPE_DATETIME: 1129 | case MYSQL_TYPE_DATETIME2: 1130 | case MYSQL_TYPE_TIMESTAMP: 1131 | case MYSQL_TYPE_TIMESTAMP2: 1132 | auto x = parseMySQLDateTime(svalue); 1133 | value = x.valid() ? MySQLValue(column.name, column.type, signed, &x, typeof(x).sizeof) : MySQLValue(column.name, ColumnTypes.MYSQL_TYPE_NULL, signed, null, 0); 1134 | break; 1135 | } 1136 | } 1137 | 1138 | void putValueType(T)(ref OutputPacket packet, T value) if (is(Unqual!T == Date) || is(Unqual!T == DateTime) || is(Unqual!T == SysTime)) { 1139 | packet.put!ubyte(ColumnTypes.MYSQL_TYPE_TIMESTAMP); 1140 | packet.put!ubyte(0x80); 1141 | } 1142 | 1143 | void putValue(T)(ref OutputPacket packet, T value) if (is(Unqual!T == Date) || is(Unqual!T == DateTime) || is(Unqual!T == SysTime)) { 1144 | putMySQLDateTime(packet, MySQLDateTime.from(value)); 1145 | } 1146 | 1147 | void putValueType(T)(ref OutputPacket packet, T value) if (is(Unqual!T == Duration)) { 1148 | packet.put!ubyte(ColumnTypes.MYSQL_TYPE_TIME); 1149 | packet.put!ubyte(0x00); 1150 | } 1151 | 1152 | void putValue(T)(ref OutputPacket packet, T value) if (is(Unqual!T == Duration)) { 1153 | putMySQLTime(packet, MySQLTime.from(value)); 1154 | } 1155 | 1156 | void putValueType(T)(ref OutputPacket packet, T value) if (isIntegral!T || isBoolean!T) { 1157 | alias UT = Unqual!T; 1158 | 1159 | enum ubyte sign = isUnsigned!UT ? 0x80 : 0x00; 1160 | 1161 | static if (is(UT == long) || is(UT == ulong)) { 1162 | packet.put!ubyte(ColumnTypes.MYSQL_TYPE_LONGLONG); 1163 | packet.put!ubyte(sign); 1164 | } else static if (is(UT == int) || is(UT == uint) || is(UT == dchar)) { 1165 | packet.put!ubyte(ColumnTypes.MYSQL_TYPE_LONG); 1166 | packet.put!ubyte(sign); 1167 | } else static if (is(UT == short) || is(UT == ushort) || is(UT == wchar)) { 1168 | packet.put!ubyte(ColumnTypes.MYSQL_TYPE_SHORT); 1169 | packet.put!ubyte(sign); 1170 | } else { 1171 | packet.put!ubyte(ColumnTypes.MYSQL_TYPE_TINY); 1172 | packet.put!ubyte(sign); 1173 | } 1174 | } 1175 | 1176 | void putValueType(T)(ref OutputPacket packet, T value) if (isFloatingPoint!T) { 1177 | alias UT = Unqual!T; 1178 | 1179 | enum ubyte sign = 0x00; 1180 | 1181 | static if (is(UT == float)) { 1182 | packet.put!ubyte(ColumnTypes.MYSQL_TYPE_FLOAT); 1183 | packet.put!ubyte(sign); 1184 | } else { 1185 | packet.put!ubyte(ColumnTypes.MYSQL_TYPE_DOUBLE); 1186 | packet.put!ubyte(sign); 1187 | } 1188 | } 1189 | 1190 | void putValue(T)(ref OutputPacket packet, T value) if (isIntegral!T || isBoolean!T) { 1191 | alias UT = Unqual!T; 1192 | 1193 | static if (is(UT == long) || is(UT == ulong)) { 1194 | packet.put!ulong(value); 1195 | } else static if (is(UT == int) || is(UT == uint) || is(UT == dchar)) { 1196 | packet.put!uint(value); 1197 | } else static if (is(UT == short) || is(UT == ushort) || is(UT == wchar)) { 1198 | packet.put!ushort(value); 1199 | } else { 1200 | packet.put!ubyte(value); 1201 | } 1202 | } 1203 | 1204 | void putValue(T)(ref OutputPacket packet, T value) if (isFloatingPoint!T) { 1205 | alias UT = Unqual!T; 1206 | 1207 | static if (is(UT == float)) { 1208 | packet.put!float(value); 1209 | } else { 1210 | packet.put!double(cast(double)value); 1211 | } 1212 | } 1213 | 1214 | void putValueType(T)(ref OutputPacket packet, T value) if (isSomeString!(OriginalType!T)) { 1215 | packet.put!ubyte(ColumnTypes.MYSQL_TYPE_STRING); 1216 | packet.put!ubyte(0x80); 1217 | } 1218 | 1219 | void putValue(T)(ref OutputPacket packet, T value) if (isSomeString!(OriginalType!T)) { 1220 | ulong size = value.length * T.init[0].sizeof; 1221 | packet.putLenEnc(size); 1222 | packet.put(value); 1223 | } 1224 | 1225 | void putValueType(T)(ref OutputPacket packet, T value) if (isArray!T && !isSomeString!(OriginalType!T)) { 1226 | foreach(ref item; value) 1227 | putValueType(packet, item); 1228 | } 1229 | 1230 | void putValue(T)(ref OutputPacket packet, T value) if (isArray!T && !isSomeString!(OriginalType!T)) { 1231 | foreach(ref item; value) 1232 | putValue(packet, item); 1233 | } 1234 | 1235 | void putValueType(T)(ref OutputPacket packet, T value) if (is(Unqual!T == MySQLBinary)) { 1236 | packet.put!ubyte(ColumnTypes.MYSQL_TYPE_BLOB); 1237 | packet.put!ubyte(0x80); 1238 | } 1239 | 1240 | void putValue(T)(ref OutputPacket packet, T value) if (is(Unqual!T == MySQLBinary)) { 1241 | ulong size = value.length; 1242 | packet.putLenEnc(size); 1243 | packet.put(value.data); 1244 | } 1245 | 1246 | void putValueType(T)(ref OutputPacket packet, T value) if(is(Unqual!T == MySQLValue)) { 1247 | packet.put!ubyte(value.type_); 1248 | packet.put!ubyte(value.sign_); 1249 | } 1250 | 1251 | void putValue(T)(ref OutputPacket packet, T value) if (is(Unqual!T == MySQLValue)) { 1252 | final switch(value.type) with (ColumnTypes) { 1253 | case MYSQL_TYPE_NULL: 1254 | break; 1255 | case MYSQL_TYPE_TINY: 1256 | packet.put!ubyte(*cast(ubyte*)value.buffer_.ptr); 1257 | break; 1258 | case MYSQL_TYPE_YEAR: 1259 | case MYSQL_TYPE_SHORT: 1260 | packet.put!ushort(*cast(ushort*)value.buffer_.ptr); 1261 | break; 1262 | case MYSQL_TYPE_INT24: 1263 | case MYSQL_TYPE_LONG: 1264 | packet.put!uint(*cast(uint*)value.buffer_.ptr); 1265 | break; 1266 | case MYSQL_TYPE_LONGLONG: 1267 | packet.put!ulong(*cast(ulong*)value.buffer_.ptr); 1268 | break; 1269 | case MYSQL_TYPE_DOUBLE: 1270 | packet.put!double(*cast(double*)value.buffer_.ptr); 1271 | break; 1272 | case MYSQL_TYPE_FLOAT: 1273 | packet.put!float(*cast(float*)value.buffer_.ptr); 1274 | break; 1275 | case MYSQL_TYPE_SET: 1276 | case MYSQL_TYPE_ENUM: 1277 | case MYSQL_TYPE_VARCHAR: 1278 | case MYSQL_TYPE_VAR_STRING: 1279 | case MYSQL_TYPE_STRING: 1280 | case MYSQL_TYPE_JSON: 1281 | case MYSQL_TYPE_NEWDECIMAL: 1282 | case MYSQL_TYPE_DECIMAL: 1283 | case MYSQL_TYPE_BIT: 1284 | case MYSQL_TYPE_TINY_BLOB: 1285 | case MYSQL_TYPE_MEDIUM_BLOB: 1286 | case MYSQL_TYPE_LONG_BLOB: 1287 | case MYSQL_TYPE_BLOB: 1288 | case MYSQL_TYPE_GEOMETRY: 1289 | packet.putLenEnc((*cast(ubyte[]*)value.buffer_.ptr).length); 1290 | packet.put(*cast(ubyte[]*)value.buffer_.ptr); 1291 | break; 1292 | case MYSQL_TYPE_TIME: 1293 | case MYSQL_TYPE_TIME2: 1294 | packet.putMySQLTime(*cast(MySQLTime*)value.buffer_.ptr); 1295 | break; 1296 | case MYSQL_TYPE_DATE: 1297 | case MYSQL_TYPE_NEWDATE: 1298 | case MYSQL_TYPE_DATETIME: 1299 | case MYSQL_TYPE_DATETIME2: 1300 | case MYSQL_TYPE_TIMESTAMP: 1301 | case MYSQL_TYPE_TIMESTAMP2: 1302 | packet.putMySQLDateTime(*cast(MySQLDateTime*)value.buffer_.ptr); 1303 | break; 1304 | } 1305 | } 1306 | 1307 | void putValueType(T)(ref OutputPacket packet, T value) if (is(Unqual!T == typeof(null))) { 1308 | packet.put!ubyte(ColumnTypes.MYSQL_TYPE_NULL); 1309 | packet.put!ubyte(0x00); 1310 | } 1311 | 1312 | void putValue(T)(ref OutputPacket packet, T value) if (is(Unqual!T == typeof(null))) { 1313 | } 1314 | 1315 | void putValueType(T)(ref OutputPacket packet, T value) if (isInstanceOf!(Nullable, T) || isInstanceOf!(NullableRef, T)) { 1316 | if (value.isNull) { 1317 | putValueType(packet, null); 1318 | } else { 1319 | putValueType(packet, value.get); 1320 | } 1321 | } 1322 | 1323 | void putValue(T)(ref OutputPacket packet, T value) if (isInstanceOf!(Nullable, T) || isInstanceOf!(NullableRef, T)) { 1324 | if (value.isNull) { 1325 | putValue(packet, null); 1326 | } else { 1327 | putValue(packet, value.get); 1328 | } 1329 | } 1330 | --------------------------------------------------------------------------------