├── .editorconfig ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── database ├── mysql │ ├── appender.d │ ├── connection.d │ ├── exception.d │ ├── inserter.d │ ├── package.d │ ├── packet.d │ ├── pool.d │ ├── protocol.d │ ├── row.d │ └── type.d ├── pool.d ├── postgresql │ ├── connection.d │ ├── db.d │ ├── exception.d │ ├── package.d │ ├── packet.d │ ├── pool.d │ ├── protocol.d │ ├── row.d │ └── type.d ├── querybuilder.d ├── row.d ├── sqlbuilder.d ├── sqlite │ ├── db.d │ └── package.d ├── traits.d └── util.d ├── dub.sdl └── source ├── mysql.d └── postgresql.d /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | end_of_line = lf 3 | charset = utf-8 4 | indent_size = 4 5 | indent_type = tab 6 | max_line_length = 120 7 | trim_trailing_whitespace = true 8 | 9 | [*.md] 10 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.project 2 | .dub 3 | dub.selections.json 4 | 5 | *.a 6 | *.lib 7 | *.pdb 8 | *.exe 9 | *.dll 10 | 11 | **/libdatabase_mysql 12 | **/libdatabase_postgresql 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: d 2 | sudo: false 3 | os: 4 | - linux 5 | - osx 6 | d: 7 | - dmd 8 | - ldc 9 | -------------------------------------------------------------------------------- /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 | [![Build Status](https://travis-ci.org/shove70/database.svg?branch=master)](https://travis-ci.org/shove70/database) 2 | [![GitHub tag](https://img.shields.io/github/tag/shove70/database.svg?maxAge=86400)](https://github.com/shove70/database/releases) 3 | [![Dub downloads](https://img.shields.io/dub/dt/database.svg)](http://code.dlang.org/packages/database) 4 | 5 | # database 6 | A lightweight native MySQL/MariaDB & PostgreSQL driver written in D. 7 | 8 | The goal is a native driver that re-uses the same buffers and the stack as much as possible, 9 | avoiding unnecessary allocations and work for the garbage collector 10 | 11 | Native. No link, No harm :) 12 | 13 | ## MySQL example 14 | ```d 15 | import std.datetime; 16 | import std.stdio; 17 | import mysql; 18 | 19 | void main() { 20 | auto conn = new Connection("127.0.0.1", "root", "pwd", "test", 3306); 21 | 22 | // change database 23 | conn.use("mewmew"); 24 | 25 | // simple insert statement 26 | conn.query("insert into users (name, email) values (?, ?)", "frank", "thetank@cowabanga.com"); 27 | auto id = conn.lastInsertId; 28 | 29 | struct User { 30 | string name; 31 | string email; 32 | } 33 | 34 | // simple select statement 35 | User[] users; 36 | conn.query("select name, email from users where id > ?", 13, (MySQLRow row) { 37 | users ~= row.get!User; 38 | }); 39 | 40 | // simple select statement 41 | conn.query("select name, email from users where id > ?", 13, (MySQLRow row) { 42 | writeln(row["name"], row["email"]); 43 | }); 44 | 45 | // batch inserter - inserts in packets of 128k bytes 46 | auto insert = inserter(conn, "users_copy", "name", "email"); 47 | foreach(user; users) 48 | insert.row(user.name, user.email); 49 | insert.flush; 50 | 51 | // re-usable prepared statements 52 | auto upd = conn.prepare("update users set sequence = ?, login_at = ?, secret = ? where id = ?"); 53 | ubyte[] bytes = [0x4D, 0x49, 0x4C, 0x4B]; 54 | foreach(i; 0..100) 55 | conn.exec(upd, i, Clock.currTime, MySQLBinary(bytes), i); 56 | 57 | // passing variable or large number of arguments 58 | string[] names; 59 | string[] emails; 60 | int[] ids = [1, 1, 3, 5, 8, 13]; 61 | conn.query("select name from users where id in " ~ ids.placeholders, ids, (MySQLRow row) { 62 | writeln(row.name.peek!(char[])); // peek() avoids allocation - cannot use result outside delegate 63 | names ~= row.name.get!string; // get() duplicates - safe to use result outside delegate 64 | emails ~= row.email.get!string; 65 | }); 66 | 67 | // another query example 68 | conn.query("select id, name, email from users where id > ?", 13, (size_t index /*optional*/, MySQLHeader header /*optional*/, MySQLRow row) { 69 | writeln(header[0].name, ": ", row.id.get!int); 70 | return (index < 5); // optionally return false to discard remaining results 71 | }); 72 | 73 | // structured row 74 | conn.query("select name, email from users where length(name) > ?", 5, (MySQLRow row) { 75 | auto user = row.get!User; // default is strict.yesIgnoreNull - a missing field in the row will throw 76 | // auto user = row.get!(User, Strict.yes); // missing or null will throw 77 | // auto user = row.get!(User, Strict.no); // missing or null will just be ignored 78 | writeln(user); 79 | }); 80 | 81 | // structured row with nested structs 82 | struct GeoRef { 83 | double lat; 84 | double lng; 85 | } 86 | 87 | struct Place { 88 | string name; 89 | GeoRef location; 90 | } 91 | 92 | conn.query("select name, lat as `location.lat`, lng as `location.lng` from places", (MySQLRow row) { 93 | auto place = row.get!Place; 94 | writeln(place.location); 95 | }); 96 | 97 | // structured row annotations 98 | struct PlaceFull { 99 | uint id; 100 | string name; 101 | @optional string thumbnail; // ok to be null or missing 102 | @optional GeoRef location; // nested fields ok to be null or missing 103 | @optional @as("contact_person") string contact; // optional, and sourced from field contact_person instead 104 | 105 | @ignore File tumbnail; // completely ignored 106 | } 107 | 108 | conn.query("select id, name, thumbnail, lat as `location.lat`, lng as `location.lng`, contact_person from places", (MySQLRow row) { 109 | auto place = row.get!PlaceFull; 110 | writeln(place.location); 111 | }); 112 | 113 | 114 | // automated struct member uncamelcase 115 | struct PlaceOwner { 116 | @snakeCase: 117 | uint placeID; // matches placeID and place_id 118 | uint locationId; // matches locationId and location_id 119 | string ownerFirstName; // matches ownerFirstName and owner_first_name 120 | string ownerLastName; // matches ownerLastName and owner_last_name 121 | string feedURL; // matches feedURL and feed_url 122 | } 123 | 124 | conn.close(); 125 | } 126 | ``` 127 | 128 | 129 | ## PGSQL example 130 | ```d 131 | import std.stdio; 132 | import postgresql; 133 | 134 | @snakeCase struct PlaceOwner { 135 | @snakeCase: 136 | @sqlkey() uint placeID; // matches place_id 137 | uint locationId; // matches location_id 138 | string ownerName; // matches owner_name 139 | string feedURL; // matches feed_url 140 | } 141 | 142 | auto db = PgSQLDB("127.0.0.1", "postgres", "postgres", "postgres"); 143 | db.runSql(`CREATE TABLE IF NOT EXISTS company( 144 | ID INT PRIMARY KEY NOT NULL, 145 | NAME TEXT NOT NULL, 146 | AGE INT NOT NULL, 147 | ADDRESS CHAR(50), 148 | SALARY REAL, 149 | JOIN_DATE DATE 150 | );`); 151 | assert(db.hasTable("company")); 152 | assert(db.query("select 42").get!int == 42); 153 | db.create!PlaceOwner; 154 | db.insert(PlaceOwner(1, 1, "foo", "")); 155 | db.insert(PlaceOwner(2, 1, "bar", "")); 156 | db.insert(PlaceOwner(3, 3, "baz", "")); 157 | auto s = db.selectOneWhere!(PlaceOwner, "owner_name=$1")("bar"); 158 | assert(s.placeID == 2); 159 | assert(s.ownerName == "bar"); 160 | foreach (row; db.selectAllWhere!(PlaceOwner, "location_id=$1")(1)) 161 | writeln(row); 162 | db.exec("drop table place_owner"); 163 | db.exec("drop table company"); 164 | db.close(); 165 | ``` 166 | 167 | ## SQLite example 168 | ```d 169 | import database.sqlite.db; 170 | 171 | auto db = SQLite3DB("file.db"); 172 | db.exec("INSERT INTO user (name, id) VALUES (?, ?)", name, id); 173 | ``` 174 | 175 | ```d 176 | struct User { 177 | ulong id; 178 | string name; 179 | void[] pixels; 180 | }; 181 | 182 | User[] users; 183 | auto q = db.query("SELECT id,name FROM user"); 184 | while(q.step()) { 185 | users ~= q.get!User; 186 | } 187 | ``` -------------------------------------------------------------------------------- /database/mysql/appender.d: -------------------------------------------------------------------------------- 1 | module database.mysql.appender; 2 | 3 | import std.datetime; 4 | import std.traits; 5 | import std.typecons; 6 | import database.mysql.type; 7 | 8 | void appendValues(R, T)(ref R appender, T values) 9 | if (isArray!T && !isSomeString!(OriginalType!T)) { 10 | foreach (i, value; values) { 11 | appendValue(appender, value); 12 | if (i != values.length - 1) 13 | appender.put(','); 14 | } 15 | } 16 | 17 | void appendValue(R)(ref R appender, typeof(null)) { 18 | appender.put("null"); 19 | } 20 | 21 | void appendValue(R, T)(ref R appender, T value) 22 | if (isInstanceOf!(Nullable, T) || isInstanceOf!(NullableRef, T)) { 23 | appendValue(appender, value.isNull ? null : value.get); 24 | } 25 | 26 | void appendValue(R, T)(ref R appender, T value) if (isScalarType!T) { 27 | import std.conv : to; 28 | 29 | appender.put(cast(ubyte[])to!string(value)); 30 | } 31 | 32 | void appendValue(R)(ref R appender, SysTime value) { 33 | value = value.toUTC; 34 | 35 | auto hour = value.hour; 36 | auto minute = value.minute; 37 | auto second = value.second; 38 | auto usec = value.fracSecs.total!"usecs"; 39 | 40 | formattedWrite(appender, "%04d%02d%02d", value.year, value.month, value.day); 41 | if (hour | minute | second | usec) { 42 | formattedWrite(appender, "%02d%02d%02d", hour, minute, second); 43 | if (usec) 44 | formattedWrite(appender, ".%06d", usec); 45 | } 46 | } 47 | 48 | void appendValue(R)(ref R appender, DateTime value) { 49 | auto hour = value.hour; 50 | auto minute = value.minute; 51 | auto second = value.second; 52 | 53 | if (hour | minute | second) { 54 | formattedWrite(appender, "%04d%02d%02d%02d%02d%02d", value.year, value.month, value.day, hour, minute, second); 55 | } else { 56 | formattedWrite(appender, "%04d%02d%02d", value.year, value.month, value.day); 57 | } 58 | } 59 | 60 | void appendValue(R)(ref R appender, TimeOfDay value) { 61 | formattedWrite(appender, "%02d%02d%02d", value.hour, value.minute, value.second); 62 | } 63 | 64 | void appendValue(R)(ref R appender, Date value) { 65 | formattedWrite(appender, "%04d%02d%02d", value.year, value.month, value.day); 66 | } 67 | 68 | void appendValue(R)(ref R appender, Duration value) { 69 | auto parts = value.split(); 70 | if (parts.days) { 71 | appender.put('\''); 72 | formattedWrite(appender, "%d ", parts.days); 73 | } 74 | formattedWrite(appender, "%02d%02d%02d", parts.hours, parts.minutes, parts.seconds); 75 | if (parts.usecs) 76 | formattedWrite(appender, ".%06d ", parts.usecs); 77 | if (parts.days) 78 | appender.put('\''); 79 | } 80 | 81 | void appendValue(R, T)(ref R appender, T value) if (is(Unqual!T == MySQLFragment)) { 82 | appender.put(cast(char[])value.data); 83 | } 84 | 85 | void appendValue(R, T)(ref R appender, T value) if (is(Unqual!T == MySQLRawString)) { 86 | appender.put('\''); 87 | appender.put(cast(char[])value.data); 88 | appender.put('\''); 89 | } 90 | 91 | void appendValue(R, T)(ref R appender, T value) if (is(Unqual!T == MySQLBinary)) { 92 | appendValue(appender, value.data); 93 | } 94 | 95 | void appendValue(R, T)(ref R appender, T value) if (is(Unqual!T == MySQLValue)) { 96 | final switch (value.type) with (ColumnTypes) { 97 | case MYSQL_TYPE_NULL: 98 | appender.put("null"); 99 | break; 100 | case MYSQL_TYPE_TINY: 101 | if (value.isSigned) { 102 | appendValue(appender, value.peek!byte); 103 | } else { 104 | appendValue(appender, value.peek!ubyte); 105 | } 106 | break; 107 | case MYSQL_TYPE_YEAR, 108 | MYSQL_TYPE_SHORT: 109 | if (value.isSigned) { 110 | appendValue(appender, value.peek!short); 111 | } else { 112 | appendValue(appender, value.peek!ushort); 113 | } 114 | break; 115 | case MYSQL_TYPE_INT24, 116 | MYSQL_TYPE_LONG: 117 | if (value.isSigned) { 118 | appendValue(appender, value.peek!int); 119 | } else { 120 | appendValue(appender, value.peek!uint); 121 | } 122 | break; 123 | case MYSQL_TYPE_LONGLONG: 124 | if (value.isSigned) { 125 | appendValue(appender, value.peek!long); 126 | } else { 127 | appendValue(appender, value.peek!ulong); 128 | } 129 | break; 130 | case MYSQL_TYPE_DOUBLE: 131 | appendValue(appender, value.peek!double); 132 | break; 133 | case MYSQL_TYPE_FLOAT: 134 | appendValue(appender, value.peek!float); 135 | break; 136 | case MYSQL_TYPE_SET, 137 | MYSQL_TYPE_ENUM, 138 | MYSQL_TYPE_VARCHAR, 139 | MYSQL_TYPE_VAR_STRING, 140 | MYSQL_TYPE_STRING, 141 | MYSQL_TYPE_JSON, 142 | MYSQL_TYPE_NEWDECIMAL, 143 | MYSQL_TYPE_DECIMAL: 144 | appendValue(appender, value.peek!(char[])); 145 | break; 146 | case MYSQL_TYPE_BIT, 147 | MYSQL_TYPE_TINY_BLOB, 148 | MYSQL_TYPE_MEDIUM_BLOB, 149 | MYSQL_TYPE_LONG_BLOB, 150 | MYSQL_TYPE_BLOB, 151 | MYSQL_TYPE_GEOMETRY: 152 | appendValue(appender, value.peek!(ubyte[])); 153 | break; 154 | case MYSQL_TYPE_TIME, 155 | MYSQL_TYPE_TIME2: 156 | appendValue(appender, value.peek!Duration); 157 | break; 158 | case MYSQL_TYPE_DATE, 159 | MYSQL_TYPE_NEWDATE, 160 | MYSQL_TYPE_DATETIME, 161 | MYSQL_TYPE_DATETIME2, 162 | MYSQL_TYPE_TIMESTAMP, 163 | MYSQL_TYPE_TIMESTAMP2: 164 | appendValue(appender, value.peek!SysTime); 165 | break; 166 | } 167 | } 168 | 169 | void appendValue(R, T)(ref R appender, T value) 170 | if (isArray!T && (is(Unqual!(typeof(T.init[0])) == ubyte) || is(Unqual!(typeof(T.init[0])) == char))) { 171 | appender.put('\''); 172 | auto ptr = value.ptr; 173 | auto end = value.ptr + value.length; 174 | while (ptr != end) { 175 | switch (*ptr) { 176 | case '\\', 177 | '\'': 178 | appender.put('\\'); 179 | goto default; 180 | default: 181 | appender.put(*ptr++); 182 | } 183 | } 184 | appender.put('\''); 185 | } 186 | -------------------------------------------------------------------------------- /database/mysql/connection.d: -------------------------------------------------------------------------------- 1 | module database.mysql.connection; 2 | 3 | import database.mysql.exception; 4 | import database.mysql.packet; 5 | import database.mysql.protocol; 6 | import database.mysql.type; 7 | import database.mysql.row; 8 | import database.mysql.appender; 9 | import database.util; 10 | import std.algorithm; 11 | import std.array; 12 | import std.conv : to; 13 | import std.string; 14 | import std.traits; 15 | import std.uni : sicmp; 16 | import std.utf : decode, UseReplacementDchar; 17 | import std.datetime; 18 | 19 | alias Socket = DBSocket!MySQLConnectionException; 20 | 21 | enum DefaultClientCaps = CF.CLIENT_LONG_PASSWORD | CF.CLIENT_LONG_FLAG | CF.CLIENT_CONNECT_WITH_DB | CF 22 | .CLIENT_PROTOCOL_41 | CF.CLIENT_SECURE_CONNECTION | CF.CLIENT_SESSION_TRACK; 23 | 24 | struct Status { 25 | ulong affected; 26 | ulong matched; 27 | ulong changed; 28 | ulong lastInsertId; 29 | ushort flags; 30 | ushort error; 31 | ushort warnings; 32 | } 33 | 34 | private: 35 | 36 | alias CF = CapabilityFlags; 37 | 38 | struct ConnectionSettings { 39 | CapabilityFlags caps = DefaultClientCaps; 40 | 41 | const(char)[] host; 42 | const(char)[] user; 43 | const(char)[] pwd; 44 | const(char)[] db; 45 | ushort port = 3306; 46 | } 47 | 48 | struct ServerInfo { 49 | const(char)[] versionStr; 50 | ubyte protocol; 51 | ubyte charSet; 52 | ushort status; 53 | uint connection; 54 | uint caps; 55 | } 56 | 57 | struct PreparedStatement { 58 | uint id; 59 | uint params; 60 | } 61 | 62 | public: 63 | 64 | class Connection { 65 | this(const(char)[] host, const(char)[] user, const(char)[] pwd, const(char)[] db, ushort port = 3306, CapabilityFlags caps = DefaultClientCaps) { 66 | settings_.host = host; 67 | settings_.user = user; 68 | settings_.pwd = pwd; 69 | settings_.db = db; 70 | settings_.port = port; 71 | settings_.caps = caps | CapabilityFlags.CLIENT_LONG_PASSWORD | CapabilityFlags 72 | .CLIENT_PROTOCOL_41; 73 | 74 | connect(); 75 | } 76 | 77 | void use(in char[] db) { 78 | send(Commands.COM_INIT_DB, db); 79 | eatStatus(retrieve()); 80 | 81 | if ((caps_ & CapabilityFlags.CLIENT_SESSION_TRACK) == 0) { 82 | schema_.length = db.length; 83 | schema_[] = db[]; 84 | } 85 | } 86 | 87 | void ping() { 88 | send(Commands.COM_PING); 89 | eatStatus(retrieve()); 90 | } 91 | 92 | void refresh() { 93 | send(Commands.COM_REFRESH); 94 | eatStatus(retrieve()); 95 | } 96 | 97 | void reset() { 98 | send(Commands.COM_RESET_CONNECTION); 99 | eatStatus(retrieve()); 100 | } 101 | 102 | const(char)[] statistics() { 103 | send(Commands.COM_STATISTICS); 104 | 105 | auto answer = retrieve(); 106 | return answer.eat!(const(char)[])(answer.remaining); 107 | } 108 | 109 | const(char)[] schema() const 110 | => schema_; 111 | 112 | auto prepare(const(char)[] sql) { 113 | send(Commands.COM_STMT_PREPARE, sql); 114 | 115 | auto answer = retrieve(); 116 | 117 | if (answer.peek!ubyte != StatusPackets.OK_Packet) 118 | eatStatus(answer); 119 | 120 | answer.expect!ubyte(0); 121 | 122 | auto id = answer.eat!uint; 123 | auto columns = answer.eat!ushort; 124 | auto params = answer.eat!ushort; 125 | answer.expect!ubyte(0); 126 | 127 | auto warnings = answer.eat!ushort; 128 | 129 | if (params) { 130 | foreach (i; 0 .. params) 131 | skipColumnDef(retrieve(), Commands.COM_STMT_PREPARE); 132 | 133 | eatEOF(retrieve()); 134 | } 135 | 136 | if (columns) { 137 | foreach (i; 0 .. columns) 138 | skipColumnDef(retrieve(), Commands.COM_STMT_PREPARE); 139 | 140 | eatEOF(retrieve()); 141 | } 142 | 143 | return PreparedStatement(id, params); 144 | } 145 | 146 | alias executeNoPrepare = query; 147 | 148 | void query(Args...)(const(char)[] sql, Args args) { 149 | //scope(failure) close(); 150 | 151 | static if (args.length == 0) { 152 | enum shouldDiscard = true; 153 | } else { 154 | enum shouldDiscard = !isCallable!(args[$ - 1]); 155 | } 156 | 157 | enum argCount = shouldDiscard ? args.length : (args.length - 1); 158 | 159 | static if (argCount) { 160 | auto querySQL = prepareSQL(sql, args[0 .. argCount]); 161 | } else { 162 | auto querySQL = sql; 163 | } 164 | 165 | send(Commands.COM_QUERY, querySQL); 166 | 167 | auto answer = retrieve(); 168 | if (isStatus(answer)) { 169 | eatStatus(answer); 170 | } else { 171 | static if (!shouldDiscard) { 172 | resultSetText(answer, Commands.COM_QUERY, args[args.length - 1]); 173 | } else { 174 | discardAll(answer, Commands.COM_QUERY); 175 | } 176 | } 177 | } 178 | 179 | void exec(Args...)(const(char)[] sql, Args args) { 180 | //scope(failure) close(); 181 | 182 | PreparedStatement stmt; 183 | if (sql in clientPreparedCaches_) { 184 | stmt = clientPreparedCaches_[sql]; 185 | } else { 186 | stmt = prepare(sql); 187 | clientPreparedCaches_[sql] = stmt; 188 | } 189 | 190 | exec(stmt, args); 191 | // closePreparedStatement(stmt); 192 | } 193 | 194 | void set(T)(const(char)[] variable, T value) { 195 | query("set session ?=?", MySQLFragment(variable), value); 196 | } 197 | 198 | const(char)[] get(const(char)[] variable) { 199 | const(char)[] result; 200 | query("show session variables like ?", variable, (MySQLRow row) { 201 | result = row[1].peek!(const(char)[]).dup; 202 | }); 203 | 204 | return result; 205 | } 206 | 207 | void startTransaction() { 208 | if (inTransaction) { 209 | throw new MySQLErrorException( 210 | "MySQL does not support nested transactions - commit or rollback before starting a new transaction"); 211 | } 212 | 213 | query("start transaction"); 214 | 215 | assert(inTransaction); 216 | } 217 | 218 | void commit() { 219 | if (!inTransaction) { 220 | throw new MySQLErrorException("No active transaction"); 221 | } 222 | 223 | query("commit"); 224 | 225 | assert(!inTransaction); 226 | } 227 | 228 | void rollback() { 229 | if (!connected) 230 | return; 231 | 232 | if ((status_.flags & StatusFlags.SERVER_STATUS_IN_TRANS) == 0) { 233 | throw new MySQLErrorException("No active transaction"); 234 | } 235 | 236 | query("rollback"); 237 | 238 | assert(!inTransaction); 239 | } 240 | 241 | @property bool inTransaction() const => connected && ( 242 | status_.flags & StatusFlags.SERVER_STATUS_IN_TRANS); 243 | 244 | void exec(Args...)(PreparedStatement stmt, Args args) { 245 | //scope(failure) close(); 246 | 247 | ensureConnected(); 248 | 249 | seq_ = 0; 250 | auto packet = OutputPacket(&out_); 251 | packet.put!ubyte(Commands.COM_STMT_EXECUTE); 252 | packet.put!uint(stmt.id); 253 | packet.put!ubyte(Cursors.CURSOR_TYPE_READ_ONLY); 254 | packet.put!uint(1); 255 | 256 | static if (args.length == 0) { 257 | enum shouldDiscard = true; 258 | } else { 259 | enum shouldDiscard = !isCallable!(args[$ - 1]); 260 | } 261 | 262 | enum argCount = shouldDiscard ? args.length : args.length - 1; 263 | 264 | if (!argCount && stmt.params) { 265 | throw new MySQLErrorException(format("Wrong number of parameters for query. Got 0 but expected %d.", stmt 266 | .params)); 267 | } 268 | 269 | static if (argCount) { 270 | enum NullsCapacity = 128; // must be power of 2 271 | ubyte[NullsCapacity >> 3] nulls; 272 | size_t bitsOut; 273 | size_t indexArg; 274 | 275 | foreach (i, arg; args[0 .. argCount]) { 276 | const auto index = (indexArg >> 3) & (NullsCapacity - 1); 277 | const auto bit = indexArg & 7; 278 | 279 | static if (is(typeof(arg) : typeof(null))) { 280 | nulls[index] = nulls[index] | (1 << bit); 281 | ++indexArg; 282 | } else static if (is(Unqual!(typeof(arg)) == MySQLValue)) { 283 | if (arg.isNull) { 284 | nulls[index] = nulls[index] | (1 << bit); 285 | } 286 | ++indexArg; 287 | } else static if (isArray!(typeof(arg)) && !isSomeString!(typeof(arg))) { 288 | indexArg += arg.length; 289 | } else { 290 | ++indexArg; 291 | } 292 | 293 | auto finishing = (i == argCount - 1); 294 | auto remaining = indexArg - bitsOut; 295 | 296 | if (finishing || (remaining >= NullsCapacity)) { 297 | while (remaining) { 298 | auto bits = min(remaining, NullsCapacity); 299 | 300 | packet.put(nulls[0 .. (bits + 7) >> 3]); 301 | bitsOut += bits; 302 | nulls[] = 0; 303 | 304 | remaining = (indexArg - bitsOut); 305 | if (!remaining || (!finishing && (remaining < NullsCapacity))) { 306 | break; 307 | } 308 | } 309 | } 310 | } 311 | packet.put!ubyte(1); 312 | 313 | if (indexArg != stmt.params) { 314 | throw new MySQLErrorException(format("Wrong number of parameters for query. Got %d but expected %d.", indexArg, stmt 315 | .params)); 316 | } 317 | 318 | foreach (arg; args[0 .. argCount]) { 319 | static if (is(typeof(arg) == enum)) { 320 | putValueType(packet, cast(OriginalType!(Unqual!(typeof(arg))))arg); 321 | } else { 322 | putValueType(packet, arg); 323 | } 324 | } 325 | 326 | foreach (arg; args[0 .. argCount]) { 327 | static if (is(typeof(arg) == enum)) { 328 | putValue(packet, cast(OriginalType!(Unqual!(typeof(arg))))arg); 329 | } else { 330 | putValue(packet, arg); 331 | } 332 | } 333 | } 334 | 335 | packet.finalize(seq_); 336 | ++seq_; 337 | 338 | socket.write(packet.get()); 339 | 340 | auto answer = retrieve(); 341 | if (isStatus(answer)) { 342 | eatStatus(answer); 343 | } else { 344 | static if (!shouldDiscard) { 345 | resultSet(answer, stmt.id, Commands.COM_STMT_EXECUTE, args[$ - 1]); 346 | } else { 347 | discardAll(answer, Commands.COM_STMT_EXECUTE); 348 | } 349 | } 350 | } 351 | 352 | void closePreparedStatement(PreparedStatement stmt) { 353 | uint[1] data = [stmt.id]; 354 | send(Commands.COM_STMT_CLOSE, data); 355 | } 356 | 357 | @property ulong lastInsertId() const => status_.lastInsertId; 358 | 359 | @property ulong affected() const => cast(size_t)status_.affected; 360 | 361 | @property ulong matched() const => cast(size_t)status_.matched; 362 | 363 | @property ulong changed() const => cast(size_t)status_.changed; 364 | 365 | @property size_t warnings() const => status_.warnings; 366 | 367 | @property size_t error() const => status_.error; 368 | 369 | @property const(char)[] status() const => info_; 370 | 371 | @property bool connected() const => socket && socket.isAlive; 372 | 373 | void close() nothrow { 374 | clearClientPreparedCache(); 375 | socket.close(); 376 | socket = null; 377 | } 378 | 379 | void reuse() { 380 | ensureConnected(); 381 | 382 | if (inTransaction) { 383 | rollback; 384 | } 385 | 386 | if (settings_.db.length && (settings_.db != schema_)) { 387 | use(settings_.db); 388 | } 389 | } 390 | 391 | @property void trace(bool enable) { 392 | trace_ = enable; 393 | } 394 | 395 | @property bool trace() => trace_; 396 | 397 | package(database): 398 | 399 | bool pooled; 400 | DateTime releaseTime; 401 | 402 | @property bool busy() const => busy_; 403 | 404 | @property void busy(bool value) { 405 | busy_ = value; 406 | 407 | if (!value) 408 | clearClientPreparedCache(); 409 | } 410 | 411 | private: 412 | 413 | void connect() { 414 | socket = new Socket(settings_.host, settings_.port); 415 | seq_ = 0; 416 | eatHandshake(retrieve()); 417 | clearClientPreparedCache(); 418 | } 419 | 420 | void send(T)(Commands cmd, T[] data) { 421 | send(cmd, cast(ubyte*)data.ptr, data.length * T.sizeof); 422 | } 423 | 424 | void send(Commands cmd, ubyte* data = null, size_t length = 0) { 425 | ensureConnected(); 426 | 427 | seq_ = 0; 428 | auto header = OutputPacket(&out_); 429 | header.put!ubyte(cmd); 430 | header.finalize(seq_, length); 431 | ++seq_; 432 | 433 | socket.write(header.get()); 434 | if (length) { 435 | socket.write(data[0 .. length]); 436 | } 437 | } 438 | 439 | void ensureConnected() { 440 | if (!connected) { 441 | connect(); 442 | } 443 | } 444 | 445 | void clearClientPreparedCache() nothrow { 446 | if (clientPreparedCaches_.length == 0) 447 | return; 448 | try 449 | foreach (p; clientPreparedCaches_) { 450 | closePreparedStatement(p); 451 | } catch (Exception) { 452 | } 453 | 454 | clientPreparedCaches_.clear(); 455 | } 456 | 457 | bool isStatus(InputPacket packet) { 458 | auto id = packet.peek!ubyte; 459 | 460 | switch (id) { 461 | case StatusPackets.ERR_Packet, 462 | StatusPackets.OK_Packet: 463 | return true; 464 | default: 465 | return false; 466 | } 467 | } 468 | 469 | void check(InputPacket packet, bool smallError = false) { 470 | auto id = packet.peek!ubyte; 471 | 472 | switch (id) { 473 | case StatusPackets.ERR_Packet, 474 | StatusPackets.OK_Packet: 475 | eatStatus(packet, smallError); 476 | break; 477 | default: 478 | break; 479 | } 480 | } 481 | 482 | InputPacket retrieve() { 483 | //scope(failure) close(); 484 | 485 | ubyte[4] header; 486 | socket.read(header); 487 | 488 | auto len = header[0] | (header[1] << 8) | (header[2] << 16); 489 | auto seq = header[3]; 490 | 491 | if (seq != seq_) { 492 | throw new MySQLConnectionException("Out of order packet received"); 493 | } 494 | 495 | ++seq_; 496 | 497 | in_.length = len; 498 | socket.read(in_); 499 | 500 | if (in_.length != len) { 501 | throw new MySQLConnectionException("Wrong number of bytes read"); 502 | } 503 | 504 | return InputPacket(&in_); 505 | } 506 | 507 | void eatHandshake(InputPacket packet) { 508 | //scope(failure) close(); 509 | 510 | check(packet, true); 511 | 512 | server_.protocol = packet.eat!ubyte; 513 | server_.versionStr = packet.eat!(const(char)[])(packet.countUntil(0, true)).dup; 514 | packet.skip(1); 515 | 516 | server_.connection = packet.eat!uint; 517 | 518 | const auto authLengthStart = 8; 519 | size_t authLength = authLengthStart; 520 | 521 | ubyte[256] auth; 522 | auth[0 .. authLength] = packet.eat!(ubyte[])(authLength); 523 | 524 | packet.expect!ubyte(0); 525 | 526 | server_.caps = packet.eat!ushort; 527 | 528 | if (!packet.empty) { 529 | server_.charSet = packet.eat!ubyte; 530 | server_.status = packet.eat!ushort; 531 | server_.caps |= packet.eat!ushort << 16; 532 | server_.caps |= CapabilityFlags.CLIENT_LONG_PASSWORD; 533 | 534 | if ((server_.caps & CapabilityFlags.CLIENT_PROTOCOL_41) == 0) { 535 | throw new MySQLProtocolException("Server doesn't support protocol v4.1"); 536 | } 537 | 538 | if (server_.caps & CapabilityFlags.CLIENT_SECURE_CONNECTION) { 539 | packet.skip(1); 540 | } else { 541 | packet.expect!ubyte(0); 542 | } 543 | 544 | packet.skip(10); 545 | 546 | authLength += packet.countUntil(0, true); 547 | if (authLength > auth.length) { 548 | throw new MySQLConnectionException("Bad packet format"); 549 | } 550 | 551 | auth[authLengthStart .. authLength] = packet.eat!(ubyte[])( 552 | authLength - authLengthStart); 553 | 554 | packet.expect!ubyte(0); 555 | } 556 | 557 | caps_ = cast(CapabilityFlags)(settings_.caps & server_.caps); 558 | 559 | ubyte[20] token; 560 | { 561 | import std.digest.sha : sha1Of; 562 | 563 | auto pass = sha1Of(cast(const(ubyte)[])settings_.pwd); 564 | token = sha1Of(auth[0 .. authLength], sha1Of(pass)); 565 | 566 | foreach (i; 0 .. 20) { 567 | token[i] = token[i] ^ pass[i]; 568 | } 569 | } 570 | 571 | auto reply = OutputPacket(&out_); 572 | 573 | reply.reserve(64 + settings_.user.length + settings_.pwd.length + settings_.db.length); 574 | 575 | reply.put!uint(caps_); 576 | reply.put!uint(1); 577 | reply.put!ubyte(45); 578 | reply.fill(23); 579 | 580 | reply.put(settings_.user); 581 | reply.put!ubyte(0); 582 | 583 | if (settings_.pwd.length) { 584 | if (caps_ & CapabilityFlags.CLIENT_SECURE_CONNECTION) { 585 | reply.put!ubyte(token.length); 586 | reply.put(token); 587 | } else { 588 | reply.put(token); 589 | reply.put!ubyte(0); 590 | } 591 | } else { 592 | reply.put!ubyte(0); 593 | } 594 | 595 | if ((settings_.db.length || schema_.length) && ( 596 | caps_ & CapabilityFlags.CLIENT_CONNECT_WITH_DB)) { 597 | if (schema_.length) { 598 | reply.put(schema_); 599 | } else { 600 | reply.put(settings_.db); 601 | 602 | schema_.length = settings_.db.length; 603 | schema_[] = settings_.db[]; 604 | } 605 | } 606 | 607 | reply.put!ubyte(0); 608 | 609 | reply.finalize(seq_); 610 | ++seq_; 611 | 612 | socket.write(reply.get()); 613 | 614 | eatStatus(retrieve()); 615 | } 616 | 617 | void eatStatus(InputPacket packet, bool smallError = false) { 618 | auto id = packet.eat!ubyte; 619 | 620 | switch (id) { 621 | case StatusPackets.OK_Packet: 622 | status_.matched = 0; 623 | status_.changed = 0; 624 | status_.affected = packet.eatLenEnc(); 625 | status_.lastInsertId = packet.eatLenEnc(); 626 | status_.flags = packet.eat!ushort; 627 | if (caps_ & CapabilityFlags.CLIENT_PROTOCOL_41) { 628 | status_.warnings = packet.eat!ushort; 629 | } 630 | status_.error = 0; 631 | info([]); 632 | 633 | if (caps_ & CapabilityFlags.CLIENT_SESSION_TRACK) { 634 | if (!packet.empty) { 635 | info(packet.eat!(const(char)[])(cast(size_t)packet.eatLenEnc())); 636 | 637 | if (status_.flags & StatusFlags.SERVER_SESSION_STATE_CHANGED) { 638 | packet.skipLenEnc(); 639 | while (!packet.empty()) { 640 | final switch (packet.eat!ubyte()) with (SessionStateType) { 641 | case SESSION_TRACK_SCHEMA: 642 | packet.skipLenEnc(); 643 | schema_.length = cast(size_t)packet.eatLenEnc(); 644 | schema_[] = packet.eat!(const(char)[])(schema_.length); 645 | break; 646 | case SESSION_TRACK_SYSTEM_VARIABLES, 647 | SESSION_TRACK_GTIDS, 648 | SESSION_TRACK_STATE_CHANGE, 649 | SESSION_TRACK_TRANSACTION_STATE, 650 | SESSION_TRACK_TRANSACTION_CHARACTERISTICS: 651 | packet.skip(cast(size_t)packet.eatLenEnc()); 652 | break; 653 | } 654 | } 655 | } 656 | } 657 | } else { 658 | info(packet.eat!(const(char)[])(packet.remaining)); 659 | } 660 | 661 | import std.regex : matchFirst, regex; 662 | 663 | static matcher = regex(`\smatched:\s*(\d+)\s+changed:\s*(\d+)`, `i`); 664 | auto matches = matchFirst(info_, matcher); 665 | 666 | if (!matches.empty) { 667 | status_.matched = matches[1].to!ulong; 668 | status_.changed = matches[2].to!ulong; 669 | } 670 | 671 | break; 672 | case StatusPackets.EOF_Packet: 673 | status_.affected = 0; 674 | status_.changed = 0; 675 | status_.matched = 0; 676 | status_.error = 0; 677 | status_.warnings = packet.eat!ushort; 678 | status_.flags = packet.eat!ushort; 679 | info([]); 680 | 681 | break; 682 | case StatusPackets.ERR_Packet: 683 | status_.affected = 0; 684 | status_.changed = 0; 685 | status_.matched = 0; 686 | //status_.flags = 0;//[shove] 687 | status_.flags &= StatusFlags.SERVER_STATUS_IN_TRANS; 688 | status_.warnings = 0; 689 | status_.error = packet.eat!ushort; 690 | if (!smallError) { 691 | packet.skip(6); 692 | } 693 | info(packet.eat!(const(char)[])(packet.remaining)); 694 | 695 | switch (status_.error) { 696 | case ErrorCodes.ER_DUP_ENTRY_WITH_KEY_NAME, 697 | ErrorCodes.ER_DUP_ENTRY: 698 | throw new MySQLDuplicateEntryException(info_.idup); 699 | case ErrorCodes.ER_DATA_TOO_LONG_FOR_COL: 700 | throw new MySQLDataTooLongException(info_.idup); 701 | case ErrorCodes.ER_DEADLOCK_FOUND: 702 | throw new MySQLDeadlockFoundException(info_.idup); 703 | case ErrorCodes.ER_TABLE_DOESNT_EXIST: 704 | throw new MySQLTableDoesntExistException(info_.idup); 705 | case ErrorCodes.ER_LOCK_WAIT_TIMEOUT: 706 | throw new MySQLLockWaitTimeoutException(info_.idup); 707 | default: 708 | version (development) { 709 | // On dev show the query together with the error message 710 | throw new MySQLErrorException(format("[err:%s] %s - %s", status_.error, info_, sql_ 711 | .data)); 712 | } else { 713 | throw new MySQLErrorException(format("[err:%s] %s", status_.error, info_)); 714 | } 715 | } 716 | default: 717 | throw new MySQLProtocolException("Unexpected packet format"); 718 | } 719 | } 720 | 721 | void info(const(char)[] value) { 722 | info_.length = value.length; 723 | info_[0 .. $] = value; 724 | } 725 | 726 | void skipColumnDef(InputPacket packet, Commands cmd) { 727 | packet.skip(cast(size_t)packet.eatLenEnc()); // catalog 728 | packet.skip(cast(size_t)packet.eatLenEnc()); // schema 729 | packet.skip(cast(size_t)packet.eatLenEnc()); // table 730 | packet.skip(cast(size_t)packet.eatLenEnc()); // original_table 731 | packet.skip(cast(size_t)packet.eatLenEnc()); // name 732 | packet.skip(cast(size_t)packet.eatLenEnc()); // original_name 733 | packet.skipLenEnc(); // next_length 734 | packet.skip(10); // 2 + 4 + 1 + 2 + 1 // charset, length, type, flags, decimals 735 | packet.expect!ushort(0); 736 | 737 | if (cmd == Commands.COM_FIELD_LIST) { 738 | packet.skip(cast(size_t)packet.eatLenEnc()); // default values 739 | } 740 | } 741 | 742 | void columnDef(InputPacket packet, Commands cmd, ref MySQLColumn def) { 743 | packet.skip(cast(size_t)packet.eatLenEnc()); // catalog 744 | packet.skip(cast(size_t)packet.eatLenEnc()); // schema 745 | packet.skip(cast(size_t)packet.eatLenEnc()); // table 746 | packet.skip(cast(size_t)packet.eatLenEnc()); // original_table 747 | auto len = cast(size_t)packet.eatLenEnc(); 748 | def.name = packet.eat!(const(char)[])(len).idup; 749 | packet.skip(cast(size_t)packet.eatLenEnc()); // original_name 750 | packet.skipLenEnc(); // next_length 751 | packet.skip(2); // charset 752 | def.length = packet.eat!uint; 753 | def.type = cast(ColumnTypes)packet.eat!ubyte; 754 | def.flags = packet.eat!ushort; 755 | def.decimals = packet.eat!ubyte; 756 | 757 | packet.expect!ushort(0); 758 | 759 | if (cmd == Commands.COM_FIELD_LIST) { 760 | packet.skip(cast(size_t)packet.eatLenEnc()); // default values 761 | } 762 | } 763 | 764 | void columnDefs(size_t count, Commands cmd, ref MySQLColumn[] defs) { 765 | defs.length = count; 766 | foreach (i; 0 .. count) { 767 | columnDef(retrieve(), cmd, defs[i]); 768 | } 769 | } 770 | 771 | bool callHandler(RowHandler)(RowHandler handler, size_t, MySQLHeader, MySQLRow row) 772 | if ((ParameterTypeTuple!(RowHandler).length == 1) && is( 773 | ParameterTypeTuple!(RowHandler)[0] == MySQLRow)) { 774 | static if (is(ReturnType!(RowHandler) == void)) { 775 | handler(row); 776 | return true; 777 | } else { 778 | return handler(row); // return type must be bool 779 | } 780 | } 781 | 782 | bool callHandler(RowHandler)(RowHandler handler, size_t i, MySQLHeader, MySQLRow row) 783 | if ((ParameterTypeTuple!(RowHandler).length == 2) && isNumeric!( 784 | ParameterTypeTuple!(RowHandler)[0]) && is( 785 | ParameterTypeTuple!(RowHandler)[1] == MySQLRow)) { 786 | static if (is(ReturnType!(RowHandler) == void)) { 787 | handler(cast(ParameterTypeTuple!(RowHandler)[0])i, row); 788 | return true; 789 | } else { 790 | return handler(cast(ParameterTypeTuple!(RowHandler)[0])i, row); // return type must be bool 791 | } 792 | } 793 | 794 | bool callHandler(RowHandler)(RowHandler handler, size_t, MySQLHeader header, MySQLRow row) 795 | if ((ParameterTypeTuple!(RowHandler).length == 2) && is( 796 | ParameterTypeTuple!(RowHandler)[0] == MySQLHeader) && is( 797 | ParameterTypeTuple!(RowHandler)[1] == MySQLRow)) { 798 | static if (is(ReturnType!(RowHandler) == void)) { 799 | handler(header, row); 800 | return true; 801 | } else { 802 | return handler(header, row); // return type must be bool 803 | } 804 | } 805 | 806 | bool callHandler(RowHandler)(RowHandler handler, size_t i, MySQLHeader header, MySQLRow row) 807 | if ((ParameterTypeTuple!(RowHandler).length == 3) && isNumeric!( 808 | ParameterTypeTuple!(RowHandler)[0]) && is(ParameterTypeTuple!(RowHandler)[1] == MySQLHeader) && is( 809 | ParameterTypeTuple!(RowHandler)[2] == MySQLRow)) { 810 | static if (is(ReturnType!(RowHandler) == void)) { 811 | handler(i, header, row); 812 | return true; 813 | } else { 814 | return handler(i, header, row); // return type must be bool 815 | } 816 | } 817 | 818 | void resultSetRow(InputPacket packet, MySQLHeader header, ref MySQLRow row) { 819 | assert(row.values.length == header.length); 820 | 821 | packet.expect!ubyte(0); 822 | auto nulls = packet.eat!(ubyte[])((header.length + 2 + 7) >> 3); 823 | 824 | foreach (i, ref column; header) { 825 | const auto index = (i + 2) >> 3; // bit offset of 2 826 | const auto bit = (i + 2) & 7; 827 | 828 | if ((nulls[index] & (1 << bit)) == 0) { 829 | row.get_(i) = eatValue(packet, column); 830 | } else { 831 | auto signed = (column.flags & FieldFlags.UNSIGNED_FLAG) == 0; 832 | row.get_(i) = MySQLValue(column.name, ColumnTypes.MYSQL_TYPE_NULL, signed, null, 0); 833 | } 834 | } 835 | assert(packet.empty); 836 | } 837 | 838 | void resultSet(RowHandler)(InputPacket packet, uint stmt, Commands cmd, RowHandler handler) { 839 | auto columns = cast(size_t)packet.eatLenEnc(); 840 | columnDefs(columns, cmd, header); 841 | row_.header(header); 842 | 843 | auto status = retrieve(); 844 | if (status.peek!ubyte == StatusPackets.ERR_Packet) { 845 | eatStatus(status); 846 | } 847 | 848 | size_t index; 849 | auto statusFlags = eatEOF(status); 850 | if (statusFlags & StatusFlags.SERVER_STATUS_CURSOR_EXISTS) { 851 | uint[2] data = [stmt, 4096]; // todo: make setting - rows per fetch 852 | while (statusFlags & ( 853 | StatusFlags.SERVER_STATUS_CURSOR_EXISTS | StatusFlags 854 | .SERVER_MORE_RESULTS_EXISTS)) { 855 | send(Commands.COM_STMT_FETCH, data); 856 | 857 | auto answer = retrieve(); 858 | if (answer.peek!ubyte == StatusPackets.ERR_Packet) { 859 | eatStatus(answer); 860 | } 861 | 862 | auto row = answer.empty ? retrieve() : answer; 863 | while (true) { 864 | if (row.peek!ubyte == StatusPackets.EOF_Packet) { 865 | statusFlags = eatEOF(row); 866 | break; 867 | } 868 | 869 | resultSetRow(row, header, row_); 870 | if (!callHandler(handler, index++, header, row_)) { 871 | discardUntilEOF(retrieve()); 872 | statusFlags = 0; 873 | break; 874 | } 875 | row = retrieve(); 876 | } 877 | } 878 | } else { 879 | while (true) { 880 | auto row = retrieve(); 881 | if (row.peek!ubyte == StatusPackets.EOF_Packet) { 882 | eatEOF(row); 883 | break; 884 | } 885 | 886 | resultSetRow(row, header, row_); 887 | if (!callHandler(handler, index++, header, row_)) { 888 | discardUntilEOF(retrieve()); 889 | break; 890 | } 891 | } 892 | } 893 | } 894 | 895 | void resultSetRowText(InputPacket packet, MySQLHeader header, ref MySQLRow row) { 896 | assert(row.values.length == header.length); 897 | 898 | foreach (i, ref column; header) { 899 | if (packet.peek!ubyte != 0xfb) { 900 | row.get_(i) = eatValueText(packet, column); 901 | } else { 902 | packet.skip(1); 903 | auto signed = (column.flags & FieldFlags.UNSIGNED_FLAG) == 0; 904 | row.get_(i) = MySQLValue(column.name, ColumnTypes.MYSQL_TYPE_NULL, signed, null, 0); 905 | } 906 | } 907 | assert(packet.empty); 908 | } 909 | 910 | void resultSetText(RowHandler)(InputPacket packet, Commands cmd, RowHandler handler) { 911 | auto columns = cast(size_t)packet.eatLenEnc(); 912 | columnDefs(columns, cmd, header); 913 | row_.header(header); 914 | 915 | eatEOF(retrieve()); 916 | 917 | size_t index; 918 | while (true) { 919 | auto row = retrieve(); 920 | if (row.peek!ubyte == StatusPackets.EOF_Packet) { 921 | eatEOF(row); 922 | break; 923 | } else if (row.peek!ubyte == StatusPackets.ERR_Packet) { 924 | eatStatus(row); 925 | break; 926 | } 927 | 928 | resultSetRowText(row, header, row_); 929 | if (!callHandler(handler, index++, header, row_)) { 930 | discardUntilEOF(retrieve()); 931 | break; 932 | } 933 | } 934 | } 935 | 936 | void discardAll(InputPacket packet, Commands cmd) { 937 | auto columns = cast(size_t)packet.eatLenEnc(); 938 | columnDefs(columns, cmd, header); 939 | 940 | auto statusFlags = eatEOF(retrieve()); 941 | if ((statusFlags & StatusFlags.SERVER_STATUS_CURSOR_EXISTS) == 0) { 942 | while (true) { 943 | auto row = retrieve(); 944 | if (row.peek!ubyte == StatusPackets.EOF_Packet) { 945 | eatEOF(row); 946 | break; 947 | } 948 | } 949 | } 950 | } 951 | 952 | void discardUntilEOF(InputPacket packet) { 953 | while (true) { 954 | if (packet.peek!ubyte == StatusPackets.EOF_Packet) { 955 | eatEOF(packet); 956 | break; 957 | } 958 | packet = retrieve(); 959 | } 960 | } 961 | 962 | auto eatEOF(InputPacket packet) { 963 | auto id = packet.eat!ubyte; 964 | if (id != StatusPackets.EOF_Packet) { 965 | throw new MySQLProtocolException("Unexpected packet format"); 966 | } 967 | 968 | status_.error = 0; 969 | status_.warnings = packet.eat!ushort(); 970 | status_.flags = packet.eat!ushort(); 971 | info([]); 972 | 973 | return status_.flags; 974 | } 975 | 976 | auto estimateArgs(Args...)(ref size_t estimated, Args args) { 977 | size_t argCount; 978 | 979 | foreach (i, arg; args) { 980 | static if (is(typeof(arg) : typeof(null))) { 981 | ++argCount; 982 | estimated += 4; 983 | } else static if (is(Unqual!(typeof(arg)) == MySQLValue)) { 984 | ++argCount; 985 | final switch (arg.type) with (ColumnTypes) { 986 | case MYSQL_TYPE_NULL: 987 | estimated += 4; 988 | break; 989 | case MYSQL_TYPE_TINY: 990 | estimated += 4; 991 | break; 992 | case MYSQL_TYPE_YEAR, 993 | MYSQL_TYPE_SHORT: 994 | estimated += 6; 995 | break; 996 | case MYSQL_TYPE_INT24, 997 | MYSQL_TYPE_LONG: 998 | estimated += 6; 999 | break; 1000 | case MYSQL_TYPE_LONGLONG: 1001 | estimated += 8; 1002 | break; 1003 | case MYSQL_TYPE_FLOAT: 1004 | estimated += 8; 1005 | break; 1006 | case MYSQL_TYPE_DOUBLE: 1007 | estimated += 8; 1008 | break; 1009 | case MYSQL_TYPE_SET, 1010 | MYSQL_TYPE_ENUM, 1011 | MYSQL_TYPE_VARCHAR, 1012 | MYSQL_TYPE_VAR_STRING, 1013 | MYSQL_TYPE_STRING, 1014 | MYSQL_TYPE_JSON, 1015 | MYSQL_TYPE_NEWDECIMAL, 1016 | MYSQL_TYPE_DECIMAL, 1017 | MYSQL_TYPE_TINY_BLOB, 1018 | MYSQL_TYPE_MEDIUM_BLOB, 1019 | MYSQL_TYPE_LONG_BLOB, 1020 | MYSQL_TYPE_BLOB, 1021 | MYSQL_TYPE_BIT, 1022 | MYSQL_TYPE_GEOMETRY: 1023 | estimated += 2 + arg.peek!(const(char)[]).length; 1024 | break; 1025 | case MYSQL_TYPE_TIME, 1026 | MYSQL_TYPE_TIME2: 1027 | estimated += 18; 1028 | break; 1029 | case MYSQL_TYPE_DATE, 1030 | MYSQL_TYPE_NEWDATE, 1031 | MYSQL_TYPE_DATETIME, 1032 | MYSQL_TYPE_DATETIME2, 1033 | MYSQL_TYPE_TIMESTAMP, 1034 | MYSQL_TYPE_TIMESTAMP2: 1035 | estimated += 20; 1036 | break; 1037 | } 1038 | } else static if (isArray!(typeof(arg)) && !isSomeString!(typeof(arg))) { 1039 | argCount += arg.length; 1040 | estimated += arg.length * 6; 1041 | } else static if (isSomeString!(typeof(arg)) || is( 1042 | Unqual!(typeof(arg)) == MySQLRawString) || is(Unqual!(typeof(arg)) == MySQLFragment) || is( 1043 | Unqual!(typeof(arg)) == MySQLBinary)) { 1044 | ++argCount; 1045 | estimated += 2 + arg.length; 1046 | } else { 1047 | ++argCount; 1048 | estimated += 6; 1049 | } 1050 | } 1051 | 1052 | return argCount; 1053 | } 1054 | 1055 | auto prepareSQL(Args...)(const(char)[] sql, Args args) { 1056 | auto estimated = sql.length; 1057 | auto argCount = estimateArgs(estimated, args); 1058 | 1059 | sql_.clear; 1060 | sql_.reserve(max(8192, estimated)); 1061 | 1062 | alias AppendFunc = bool function(ref Appender!(char[]), ref const(char)[] sql, ref size_t, const( 1063 | void)*) @safe pure nothrow; 1064 | AppendFunc[Args.length] funcs; 1065 | const(void)*[Args.length] addrs; 1066 | 1067 | foreach (i, Arg; Args) { 1068 | static if (is(Arg == enum)) { 1069 | funcs[i] = () @trusted { 1070 | return cast(AppendFunc)&appendNextValue!(OriginalType!Arg); 1071 | }(); 1072 | addrs[i] = (ref x) @trusted { return cast(const void*)&x; }( 1073 | cast(OriginalType!(Unqual!Arg))args[i]); 1074 | } else { 1075 | funcs[i] = () @trusted { 1076 | return cast(AppendFunc)&appendNextValue!(Arg); 1077 | }(); 1078 | addrs[i] = (ref x) @trusted { return cast(const void*)&x; }(args[i]); 1079 | } 1080 | } 1081 | 1082 | size_t indexArg; 1083 | foreach (i; 0 .. Args.length) { 1084 | if (!funcs[i](sql_, sql, indexArg, addrs[i])) { 1085 | throw new MySQLErrorException(format("Wrong number of parameters for query. Got %d but expected %d.", argCount, indexArg)); 1086 | } 1087 | } 1088 | 1089 | finishCopy(sql_, sql, argCount, indexArg); 1090 | 1091 | return sql_.data; 1092 | } 1093 | 1094 | void finishCopy(ref Appender!(char[]) app, ref const(char)[] sql, size_t argCount, size_t indexArg) { 1095 | if (copyUpToNext(sql_, sql)) { 1096 | ++indexArg; 1097 | 1098 | while (copyUpToNext(sql_, sql)) { 1099 | ++indexArg; 1100 | } 1101 | 1102 | throw new MySQLErrorException(format("Wrong number of parameters for query. Got %d but expected %d.", argCount, indexArg)); 1103 | } 1104 | } 1105 | 1106 | Socket socket; 1107 | MySQLHeader header; 1108 | MySQLRow row_; 1109 | char[] info_; 1110 | char[] schema_; 1111 | ubyte[] in_; 1112 | ubyte[] out_; 1113 | ubyte seq_; 1114 | Appender!(char[]) sql_; 1115 | 1116 | CapabilityFlags caps_; 1117 | Status status_; 1118 | ConnectionSettings settings_; 1119 | ServerInfo server_; 1120 | 1121 | // For tracing queries 1122 | bool trace_; 1123 | 1124 | PreparedStatement[const(char)[]] clientPreparedCaches_; 1125 | 1126 | bool busy_; 1127 | } 1128 | 1129 | private auto copyUpToNext(ref Appender!(char[]) app, ref const(char)[] sql) { 1130 | size_t offset; 1131 | dchar quote = '\0'; 1132 | 1133 | while (offset < sql.length) { 1134 | auto ch = decode!(UseReplacementDchar.no)(sql, offset); 1135 | switch (ch) { 1136 | case '?': 1137 | if (!quote) { 1138 | app.put(sql[0 .. offset - 1]); 1139 | sql = sql[offset .. $]; 1140 | return true; 1141 | } else { 1142 | goto default; 1143 | } 1144 | case '\'', 1145 | '\"', 1146 | '`': 1147 | if (quote == ch) { 1148 | quote = '\0'; 1149 | } else if (!quote) { 1150 | quote = ch; 1151 | } 1152 | goto default; 1153 | case '\\': 1154 | if (quote && (offset < sql.length)) 1155 | decode!(UseReplacementDchar.no)(sql, offset); 1156 | goto default; 1157 | default: 1158 | break; 1159 | } 1160 | } 1161 | app.put(sql[0 .. offset]); 1162 | sql = sql[offset .. $]; 1163 | 1164 | return false; 1165 | } 1166 | 1167 | private bool appendNextValue(T)(ref Appender!(char[]) app, ref const(char)[] sql, ref size_t indexArg, const( 1168 | void)* arg) { 1169 | static if (isArray!T && !isSomeString!(OriginalType!T)) { 1170 | foreach (i, ref v; *cast(T*)arg) { 1171 | if (!copyUpToNext(app, sql)) 1172 | return false; 1173 | appendValue(app, v); 1174 | ++indexArg; 1175 | } 1176 | } else { 1177 | if (!copyUpToNext(app, sql)) 1178 | return false; 1179 | appendValue(app, *cast(T*)arg); 1180 | ++indexArg; 1181 | } 1182 | 1183 | return true; 1184 | } 1185 | -------------------------------------------------------------------------------- /database/mysql/exception.d: -------------------------------------------------------------------------------- 1 | module database.mysql.exception; 2 | 3 | import database.util : DBException; 4 | 5 | @safe: 6 | 7 | class MySQLException : DBException { 8 | this(string msg, string file = __FILE__, size_t line = __LINE__) pure { 9 | super(msg, file, line); 10 | } 11 | } 12 | 13 | class MySQLConnectionException : MySQLException { 14 | this(string msg, string file = __FILE__, size_t line = __LINE__) pure { 15 | super(msg, file, line); 16 | } 17 | } 18 | 19 | class MySQLProtocolException : MySQLException { 20 | this(string msg, string file = __FILE__, size_t line = __LINE__) pure { 21 | super(msg, file, line); 22 | } 23 | } 24 | 25 | class MySQLErrorException : DBException { 26 | this(string msg, string file = __FILE__, size_t line = __LINE__) pure { 27 | super(msg, file, line); 28 | } 29 | } 30 | 31 | class MySQLDuplicateEntryException : MySQLErrorException { 32 | this(string msg, string file = __FILE__, size_t line = __LINE__) pure { 33 | super(msg, file, line); 34 | } 35 | } 36 | 37 | class MySQLDataTooLongException : MySQLErrorException { 38 | this(string msg, string file = __FILE__, size_t line = __LINE__) pure { 39 | super(msg, file, line); 40 | } 41 | } 42 | 43 | class MySQLDeadlockFoundException : MySQLErrorException { 44 | this(string msg, string file = __FILE__, size_t line = __LINE__) pure { 45 | super(msg, file, line); 46 | } 47 | } 48 | 49 | class MySQLTableDoesntExistException : MySQLErrorException { 50 | this(string msg, string file = __FILE__, size_t line = __LINE__) pure { 51 | super(msg, file, line); 52 | } 53 | } 54 | 55 | class MySQLLockWaitTimeoutException : MySQLErrorException { 56 | this(string msg, string file = __FILE__, size_t line = __LINE__) pure { 57 | super(msg, file, line); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /database/mysql/inserter.d: -------------------------------------------------------------------------------- 1 | module database.mysql.inserter; 2 | 3 | // dfmt off 4 | import 5 | database.mysql.appender, 6 | database.mysql.connection, 7 | database.mysql.exception, 8 | database.mysql.type, 9 | database.util, 10 | std.array, 11 | std.meta, 12 | std.range, 13 | std.string, 14 | std.traits; 15 | // dfmt on 16 | 17 | private { 18 | alias E = MySQLErrorException; 19 | enum isSomeStringOrStringArray(T) = isSomeString!(OriginalType!T) || 20 | (isArray!T && isSomeString!(ElementType!T)); 21 | enum allStringOrStringArray(T...) = T.length && allSatisfy!(isSomeStringOrStringArray, T); 22 | enum quote = '`'; 23 | } 24 | 25 | enum OnDuplicate { 26 | ignore = "insert ignore into ", 27 | fail = "insert into ", 28 | replace = "replace into ", 29 | updateAll = "UpdateAll" 30 | } 31 | 32 | auto inserter(Connection connection) => Inserter(connection); 33 | 34 | auto inserter(Args...)(Connection connection, OnDuplicate action, string tableName, Args columns) { 35 | auto insert = Inserter(connection); 36 | insert.start(action, tableName, columns); 37 | return insert; 38 | } 39 | 40 | auto inserter(Args...)(Connection connection, string tableName, Args columns) { 41 | auto insert = Inserter(connection); 42 | insert.start(OnDuplicate.fail, tableName, columns); 43 | return insert; 44 | } 45 | 46 | struct Inserter { 47 | @disable this(); 48 | @disable this(this); 49 | 50 | this(Connection connection) 51 | in (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) 62 | if (allStringOrStringArray!Args) { 63 | start(OnDuplicate.fail, tableName, fieldNames); 64 | } 65 | 66 | void start(Args...)(OnDuplicate action, string tableName, Args fieldNames) 67 | if (allStringOrStringArray!Args) { 68 | auto fieldCount = fieldNames.length; 69 | 70 | foreach (size_t i, Arg; Args) { 71 | static if (isArray!Arg && !isSomeString!(OriginalType!Arg)) 72 | fieldCount = (fieldCount - 1) + fieldNames[i].length; 73 | } 74 | 75 | fields_ = fieldCount; 76 | 77 | Appender!(char[]) app; 78 | 79 | if (action == OnDuplicate.updateAll) { 80 | Appender!(char[]) dupapp; 81 | 82 | foreach (i, Arg; Args) { 83 | static if (isSomeString!(OriginalType!Arg)) { 84 | dupapp ~= quote; 85 | dupapp ~= fieldNames[i]; 86 | dupapp ~= quote; 87 | dupapp ~= "=values("; 88 | dupapp ~= quote; 89 | dupapp ~= fieldNames[i]; 90 | dupapp ~= quote; 91 | dupapp ~= ')'; 92 | } else { 93 | auto columns = fieldNames[i]; 94 | foreach (j, name; columns) { 95 | dupapp ~= quote; 96 | dupapp ~= name; 97 | dupapp ~= quote; 98 | dupapp ~= "=values("; 99 | dupapp ~= quote; 100 | dupapp ~= name; 101 | dupapp ~= quote; 102 | dupapp ~= ')'; 103 | if (j + 1 != columns.length) 104 | dupapp ~= ','; 105 | } 106 | } 107 | if (i + 1 != Args.length) 108 | dupapp ~= ','; 109 | } 110 | dupUpdate = dupapp[]; 111 | } 112 | app ~= cast(char[])action; 113 | 114 | app ~= tableName; 115 | app ~= '('; 116 | 117 | foreach (i, Arg; Args) { 118 | static if (isSomeString!(OriginalType!Arg)) { 119 | fieldsHash ~= hashOf(fieldNames[i]); 120 | fieldsNames ~= fieldNames[i]; 121 | 122 | app ~= quote; 123 | app ~= fieldNames[i]; 124 | app ~= quote; 125 | } else { 126 | auto columns = fieldNames[i]; 127 | foreach (j, name; columns) { 128 | fieldsHash ~= hashOf(name); 129 | fieldsNames ~= name; 130 | 131 | app ~= quote; 132 | app ~= name; 133 | app ~= quote; 134 | if (j + 1 != columns.length) 135 | app ~= ','; 136 | } 137 | } 138 | if (i + 1 != Args.length) 139 | app ~= ','; 140 | } 141 | 142 | app ~= ")values"; 143 | start_ = app[]; 144 | } 145 | 146 | auto ref duplicateUpdate(string update) { 147 | dupUpdate = cast(char[])update; 148 | return this; 149 | } 150 | 151 | void rows(T)(ref const T[] param) if (!isValueType!T) { 152 | foreach (ref p; param) 153 | row(p); 154 | } 155 | 156 | private bool tryAppendField(string member, string parentMembers = "", T)( 157 | ref const T param, ref size_t fieldHash) { 158 | static if (isReadableDataMember!(__traits(getMember, Unqual!T, member))) { 159 | alias memberType = typeof(__traits(getMember, param, member)); 160 | static if (isValueType!memberType) { 161 | enum nameHash = hashOf(parentMembers ~ SQLName!(__traits(getMember, param, member), member)); 162 | if (nameHash == fieldHash) { 163 | appendValue(values_, __traits(getMember, param, member)); 164 | return true; 165 | } 166 | } else 167 | foreach (subMember; __traits(allMembers, memberType)) { 168 | if (tryAppendField!(subMember, parentMembers ~ member ~ '.')(__traits(getMember, param, member), fieldHash)) 169 | return true; 170 | } 171 | } 172 | return false; 173 | } 174 | 175 | void row(T)(ref const T param) if (!isValueType!T) { 176 | scope (failure) 177 | reset(); 178 | 179 | if (start_ == []) 180 | throw new E("Inserter must be initialized with a call to start()"); 181 | 182 | if (!pending_) 183 | values_ ~= start_; 184 | 185 | values_ ~= pending_ ? ",(" : "("; 186 | ++pending_; 187 | 188 | foreach (i, ref fieldHash; fieldsHash) { 189 | bool fieldFound; 190 | foreach (member; __traits(allMembers, T)) { 191 | fieldFound = tryAppendField!member(param, fieldHash); 192 | if (fieldFound) 193 | break; 194 | } 195 | if (!fieldFound) 196 | throw new E("field '%s' was not found in struct => '%s' members".format(fieldsNames.ptr[i], ( 197 | Unqual!T).stringof)); 198 | 199 | if (i != fields_ - 1) 200 | values_ ~= ','; 201 | } 202 | values_ ~= ')'; 203 | 204 | if (values_[].length > (128 << 10)) // todo: make parameter 205 | flush(); 206 | 207 | ++rows_; 208 | } 209 | 210 | void row(Values...)(Values values) if (allSatisfy!(isValueType, Values)) { 211 | scope (failure) 212 | reset(); 213 | 214 | if (start_ == []) 215 | throw new E("Inserter must be initialized with a call to start()"); 216 | 217 | auto valueCount = Values.length; 218 | 219 | foreach (i, Value; Values) { 220 | static if (isArray!Value && !isSomeString!(OriginalType!Value)) 221 | valueCount += values[i].length - 1; 222 | } 223 | 224 | if (valueCount != fields_) 225 | throw new E("Wrong number of parameters for row. Got %d but expected %d.".format(valueCount, fields_)); 226 | 227 | if (!pending_) 228 | values_ ~= start_; 229 | 230 | values_ ~= pending_ ? ",(" : "("; 231 | ++pending_; 232 | foreach (i, Value; Values) { 233 | static if (isArray!Value && !isSomeString!(OriginalType!Value)) 234 | appendValues(values_, values[i]); 235 | else 236 | appendValue(values_, values[i]); 237 | if (i != values.length - 1) 238 | values_ ~= ','; 239 | } 240 | values_ ~= ')'; 241 | 242 | if (values_[].length > bufferSize) // todo: make parameter 243 | flush(); 244 | 245 | ++rows_; 246 | } 247 | 248 | @property { 249 | size_t rows() const => rows_ != 0; 250 | 251 | size_t pending() const => pending_ != 0; 252 | 253 | size_t flushes() const => flushes_; 254 | } 255 | 256 | void flush() { 257 | if (pending_) { 258 | if (dupUpdate.length) { 259 | values_ ~= " on duplicate key update "; 260 | values_ ~= dupUpdate; 261 | } 262 | 263 | auto sql = values_[]; 264 | reset(); 265 | 266 | conn.query(cast(string)sql); 267 | ++flushes_; 268 | } 269 | } 270 | 271 | size_t bufferSize = 128 << 10; 272 | 273 | private: 274 | void reset() { 275 | values_.clear; 276 | pending_ = 0; 277 | } 278 | 279 | char[] start_, dupUpdate; 280 | Appender!(char[]) values_; 281 | 282 | Connection conn; 283 | size_t pending_, 284 | flushes_, 285 | fields_, 286 | rows_; 287 | string[] fieldsNames; 288 | size_t[] fieldsHash; 289 | } 290 | 291 | @property { 292 | string placeholders(size_t x, bool parens = true) { 293 | import std.array; 294 | 295 | if (!x) 296 | return null; 297 | auto app = appender!string; 298 | if (parens) { 299 | app.reserve((x << 1) - 1); 300 | 301 | app ~= '('; 302 | foreach (i; 0 .. x - 1) 303 | app ~= "?,"; 304 | app ~= '?'; 305 | app ~= ')'; 306 | } else { 307 | app.reserve(x << 1 | 1); 308 | 309 | foreach (i; 0 .. x - 1) 310 | app ~= "?,"; 311 | app ~= '?'; 312 | } 313 | return app[]; 314 | } 315 | 316 | string placeholders(T)(T x, bool parens = true) 317 | if (is(typeof(() { auto y = x.length; }))) 318 | => x.length.placeholders(parens); 319 | } 320 | -------------------------------------------------------------------------------- /database/mysql/package.d: -------------------------------------------------------------------------------- 1 | module database.mysql; 2 | 3 | public import 4 | database.mysql.connection, 5 | database.mysql.exception, 6 | database.mysql.inserter, 7 | database.mysql.pool, 8 | database.mysql.protocol, 9 | database.mysql.row, 10 | database.mysql.type; 11 | -------------------------------------------------------------------------------- /database/mysql/packet.d: -------------------------------------------------------------------------------- 1 | module database.mysql.packet; 2 | 3 | import std.algorithm; 4 | import std.traits; 5 | import database.mysql.exception; 6 | import database.util; 7 | 8 | struct InputPacket { 9 | @disable this(); 10 | 11 | this(ubyte[]* buffer) { 12 | buf = *buffer; 13 | } 14 | 15 | T peek(T)() if (!isArray!T) { 16 | assert(T.sizeof <= buf.length); 17 | return *(cast(T*)buf.ptr); 18 | } 19 | 20 | T eat(T)() if (!isArray!T) { 21 | assert(T.sizeof <= buf.length); 22 | auto ptr = cast(T*)buf.ptr; 23 | buf = buf[T.sizeof .. $]; 24 | return *ptr; 25 | } 26 | 27 | T peek(T)(size_t count) if (isArray!T) { 28 | alias ValueType = typeof(Type.init[0]); 29 | 30 | assert(ValueType.sizeof * count <= buf.length); 31 | auto ptr = cast(ValueType*)buf.ptr; 32 | return ptr[0 .. count]; 33 | } 34 | 35 | T eat(T)(size_t count) if (isArray!T) { 36 | alias ValueType = typeof(T.init[0]); 37 | 38 | assert(ValueType.sizeof * count <= buf.length); 39 | auto ptr = cast(ValueType*)buf.ptr; 40 | buf = buf[ValueType.sizeof * count .. $]; 41 | return ptr[0 .. count]; 42 | } 43 | 44 | mixin InputPacketMethods!MySQLProtocolException; 45 | 46 | private: 47 | ubyte[] buf; 48 | } 49 | 50 | struct OutputPacket { 51 | @disable this(); 52 | @disable this(this); 53 | 54 | this(ubyte[]* buffer) { 55 | buf = buffer; 56 | out_ = buf.ptr + 4; 57 | } 58 | 59 | pragma(inline, true) void put(T)(T x) { 60 | put(pos, x); 61 | } 62 | 63 | void put(T)(size_t offset, T x) if (!isArray!T) { 64 | grow(offset, T.sizeof); 65 | 66 | *(cast(T*)(out_ + offset)) = x; 67 | pos = max(offset + T.sizeof, pos); 68 | } 69 | 70 | void put(T)(size_t offset, T x) if (isArray!T) { 71 | alias ValueType = Unqual!(typeof(T.init[0])); 72 | 73 | grow(offset, ValueType.sizeof * x.length); 74 | 75 | (cast(ValueType*)(out_ + offset))[0 .. x.length] = x; 76 | pos = max(offset + (ValueType.sizeof * x.length), pos); 77 | } 78 | 79 | size_t marker(T)() if (!isArray!T) { 80 | grow(pos, T.sizeof); 81 | 82 | auto place = pos; 83 | pos += T.sizeof; 84 | return place; 85 | } 86 | 87 | size_t marker(T)() if (isArray!T) { 88 | alias ValueType = Unqual!(typeof(T.init[0])); 89 | grow(pos, ValueType.sizeof * x.length); 90 | 91 | auto place = pos; 92 | pos += ValueType.sizeof * x.length; 93 | return place; 94 | } 95 | 96 | void finalize(ubyte seq) { 97 | if (pos >= 0xffffff) 98 | throw new MySQLConnectionException("Packet size exceeds 2^24"); 99 | uint header = cast(uint)((pos & 0xffffff) | (seq << 24)); 100 | *(cast(uint*)buf.ptr) = header; 101 | } 102 | 103 | void finalize(ubyte seq, size_t extra) { 104 | if (pos + extra >= 0xffffff) 105 | throw new MySQLConnectionException("Packet size exceeds 2^24"); 106 | uint length = cast(uint)(pos + extra); 107 | uint header = cast(uint)((length & 0xffffff) | (seq << 24)); 108 | *(cast(uint*)buf.ptr) = header; 109 | } 110 | 111 | void reserve(size_t size) { 112 | (*buf).length = max((*buf).length, 4 + size); 113 | out_ = buf.ptr + 4; 114 | } 115 | 116 | const(ubyte)[] get() const 117 | => (*buf)[0 .. 4 + pos]; 118 | 119 | void fill(size_t size) @trusted { 120 | static if (is(typeof(grow))) 121 | grow(pos, size); 122 | out_[pos .. pos + size] = 0; 123 | pos += size; 124 | } 125 | 126 | mixin OutputPacketMethods; 127 | 128 | private: 129 | void grow(size_t offset, size_t size) { 130 | auto requested = 4 + offset + size; 131 | 132 | if (requested > buf.length) { 133 | auto capacity = max(128, (*buf).capacity); 134 | while (capacity < requested) 135 | capacity <<= 1; 136 | 137 | buf.length = capacity; 138 | out_ = buf.ptr + 4; 139 | } 140 | } 141 | 142 | ubyte[]* buf; 143 | ubyte* out_; 144 | size_t pos; 145 | } 146 | -------------------------------------------------------------------------------- /database/mysql/pool.d: -------------------------------------------------------------------------------- 1 | module database.mysql.pool; 2 | 3 | import database.mysql.connection; 4 | import database.mysql.protocol; 5 | import database.pool; 6 | 7 | alias ConnectionPool = shared ConnectionProvider!(Connection, DefaultClientCaps); 8 | -------------------------------------------------------------------------------- /database/mysql/protocol.d: -------------------------------------------------------------------------------- 1 | module database.mysql.protocol; 2 | 3 | enum CapabilityFlags : uint 4 | { 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 | enum StatusFlags : ushort 33 | { 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 | enum SessionStateType : ubyte 51 | { 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 | enum StatusPackets : ubyte 61 | { 62 | OK_Packet = 0, 63 | ERR_Packet = 0xff, 64 | EOF_Packet = 0xfe, 65 | } 66 | 67 | enum Commands : ubyte 68 | { 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 | enum Cursors : ubyte 104 | { 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 | enum ColumnTypes : ubyte 112 | { 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 | auto columnTypeName(ColumnTypes type) 147 | { 148 | final switch (type) with (ColumnTypes) 149 | { 150 | case MYSQL_TYPE_DECIMAL: return "decimal"; 151 | case MYSQL_TYPE_TINY: return "tiny"; 152 | case MYSQL_TYPE_SHORT: return "short"; 153 | case MYSQL_TYPE_LONG: return "long"; 154 | case MYSQL_TYPE_FLOAT: return "float"; 155 | case MYSQL_TYPE_DOUBLE: return "double"; 156 | case MYSQL_TYPE_NULL: return "null"; 157 | case MYSQL_TYPE_TIMESTAMP: return "timestamp"; 158 | case MYSQL_TYPE_LONGLONG: return "longlong"; 159 | case MYSQL_TYPE_INT24: return "int24"; 160 | case MYSQL_TYPE_DATE: return "date"; 161 | case MYSQL_TYPE_TIME: return "time"; 162 | case MYSQL_TYPE_DATETIME: return "datetime"; 163 | case MYSQL_TYPE_YEAR: return "year"; 164 | case MYSQL_TYPE_NEWDATE: return "newdate"; 165 | case MYSQL_TYPE_VARCHAR: return "varchar"; 166 | case MYSQL_TYPE_BIT: return "bit"; 167 | case MYSQL_TYPE_TIMESTAMP2: return "timestamp2"; 168 | case MYSQL_TYPE_DATETIME2: return "datetime2"; 169 | case MYSQL_TYPE_TIME2: return "time2"; 170 | case MYSQL_TYPE_JSON: return "json"; 171 | case MYSQL_TYPE_NEWDECIMAL: return "newdecimal"; 172 | case MYSQL_TYPE_ENUM: return "enum"; 173 | case MYSQL_TYPE_SET: return "set"; 174 | case MYSQL_TYPE_TINY_BLOB: return "tiny_blob"; 175 | case MYSQL_TYPE_MEDIUM_BLOB: return "medium_blob"; 176 | case MYSQL_TYPE_LONG_BLOB: return "long_blob"; 177 | case MYSQL_TYPE_BLOB: return "blob"; 178 | case MYSQL_TYPE_VAR_STRING: return "var_string"; 179 | case MYSQL_TYPE_STRING: return "string"; 180 | case MYSQL_TYPE_GEOMETRY: return "geometry"; 181 | } 182 | } 183 | 184 | enum FieldFlags : ushort 185 | { 186 | NOT_NULL_FLAG = 0x0001, // Field cannot be NULL 187 | PRI_KEY_FLAG = 0x0002, // Field is part of a primary key 188 | UNIQUE_KEY_FLAG = 0x0004, // Field is part of a unique key 189 | MULTIPLE_KEY_FLAG = 0x0008, // Field is part of a nonunique key 190 | BLOB_FLAG = 0x0010, // Field is a BLOB or TEXT (deprecated) 191 | UNSIGNED_FLAG = 0x0020, // Field has the UNSIGNED attribute 192 | ZEROFILL_FLAG = 0x0040, // Field has the ZEROFILL attribute 193 | BINARY_FLAG = 0x0080, // Field has the BINARY attribute 194 | ENUM_FLAG = 0x0100, // Field is an ENUM 195 | AUTO_INCREMENT_FLAG = 0x0200, // Field has the AUTO_INCREMENT attribute 196 | TIMESTAMP_FLAG = 0x0400, // Field is a TIMESTAMP (deprecated) 197 | SET_FLAG = 0x0800, // Field is a SET 198 | NO_DEFAULT_VALUE_FLAG = 0x1000, // Field has no default value; see additional notes following table 199 | ON_UPDATE_NOW_FLAG = 0x2000, // Field is set to NOW on UPDATE 200 | // PART_KEY_FLAG = 0x4000, // Intern; Part of some key 201 | NUM_FLAG = 0x8000, // Field is numeric 202 | } 203 | 204 | enum ErrorCodes : ushort 205 | { 206 | ER_DUP_KEYNAME = 1061, 207 | ER_DUP_ENTRY = 1062, 208 | ER_DUP_ENTRY_WITH_KEY_NAME = 1586, 209 | ER_DEADLOCK_FOUND = 1213, 210 | ER_DATA_TOO_LONG_FOR_COL = 1406, 211 | ER_TABLE_DOESNT_EXIST = 1146, 212 | ER_LOCK_WAIT_TIMEOUT = 1205, 213 | } 214 | -------------------------------------------------------------------------------- /database/mysql/row.d: -------------------------------------------------------------------------------- 1 | module database.mysql.row; 2 | 3 | import database.mysql.exception; 4 | import database.mysql.type; 5 | public import database.row; 6 | 7 | private uint hashOf(const(char)[] x) 8 | { 9 | import std.ascii; 10 | 11 | uint hash = 2166136261u; 12 | foreach(i; 0..x.length) 13 | hash = (hash ^ cast(uint)(toLower(x.ptr[i]))) * 16777619u; 14 | 15 | return hash; 16 | } 17 | 18 | alias MySQLRow = Row!(MySQLValue, MySQLHeader, MySQLErrorException, hashOf, Mixin); 19 | 20 | private template Mixin() 21 | { 22 | private static bool equalsCI(const(char)[] x, const(char)[] y) 23 | { 24 | import std.ascii; 25 | 26 | if (x.length != y.length) 27 | return false; 28 | 29 | foreach(i; 0..x.length) 30 | if (toLower(x.ptr[i]) != toLower(y.ptr[i])) 31 | return false; 32 | 33 | return true; 34 | } 35 | 36 | package uint find(uint hash, const(char)[] key) const 37 | { 38 | if (auto mask = index_.length - 1) { 39 | assert((index_.length & mask) == 0); 40 | 41 | hash = hash & mask; 42 | uint probe; 43 | 44 | for (;;) { 45 | auto index = index_[hash]; 46 | if (!index) 47 | break; 48 | if (equalsCI(_header[index - 1].name, key)) 49 | return index; 50 | hash = (hash + ++probe) & mask; 51 | } 52 | } 53 | 54 | return 0; 55 | } 56 | 57 | private void structurize(Strict strict = Strict.yesIgnoreNull, string path = null, T)(ref T result) 58 | { 59 | import database.mysql.exception; 60 | import database.mysql.type; 61 | import database.util; 62 | import std.format : format; 63 | 64 | enum unCamel = hasUDA!(T, snakeCase); 65 | foreach(member; __traits(allMembers, T)) 66 | { 67 | static if (isWritableDataMember!(__traits(getMember, T, member))) 68 | { 69 | static if (!hasUDA!(__traits(getMember, result, member), as)) 70 | { 71 | enum pathMember = path ~ member; 72 | static if (unCamel) 73 | { 74 | enum pathMemberAlt = path ~ member.snakeCase; 75 | } 76 | } 77 | else 78 | { 79 | enum pathMember = path ~ getUDAs!(__traits(getMember, result, member), as)[0].name; 80 | static if (unCamel) 81 | { 82 | enum pathMemberAlt = pathMember; 83 | } 84 | } 85 | 86 | alias MemberType = typeof(__traits(getMember, result, member)); 87 | 88 | static if (isPointer!MemberType && !isValueType!(PointerTarget!MemberType) || !isValueType!MemberType) 89 | { 90 | enum pathNew = pathMember ~ '.'; 91 | enum st = Select!(hasUDA!(__traits(getMember, result, member), optional), Strict.no, strict); 92 | static if (isPointer!MemberType) 93 | { 94 | if (__traits(getMember, result, member)) 95 | structurize!(st, pathNew)(*__traits(getMember, result, member)); 96 | } 97 | else 98 | { 99 | structurize!(st, pathNew)(__traits(getMember, result, member)); 100 | } 101 | } 102 | else 103 | { 104 | enum hash = pathMember.hashOf; 105 | static if (unCamel) 106 | { 107 | enum hashAlt = pathMemberAlt.hashOf; 108 | } 109 | 110 | auto index = find(hash, pathMember); 111 | static if (unCamel && (pathMember != pathMemberAlt)) 112 | { 113 | if (!index) 114 | index = find(hashAlt, pathMemberAlt); 115 | } 116 | 117 | if (index) 118 | { 119 | auto pvalue = values[index - 1]; 120 | 121 | static if (strict == Strict.no || strict == Strict.yesIgnoreNull || hasUDA!(__traits(getMember, result, member), optional)) 122 | { 123 | if (pvalue.isNull) 124 | continue; 125 | } 126 | 127 | __traits(getMember, result, member) = pvalue.get!(Unqual!MemberType); 128 | continue; 129 | } 130 | 131 | static if ((strict == Strict.yes || strict == Strict.yesIgnoreNull) && !hasUDA!(__traits(getMember, result, member), optional)) 132 | { 133 | static if (!unCamel || (pathMember == pathMemberAlt)) 134 | { 135 | enum ColumnError = "Column '%s' was not found in this result set".format(pathMember); 136 | } 137 | else 138 | { 139 | enum ColumnError = "Column '%s' or '%s' was not found in this result set".format(pathMember, pathMemberAlt); 140 | } 141 | throw new MySQLErrorException(ColumnError); 142 | } 143 | } 144 | } 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /database/pool.d: -------------------------------------------------------------------------------- 1 | module database.pool; 2 | 3 | import core.thread; 4 | import std.concurrency; 5 | import std.datetime; 6 | import std.exception : enforce; 7 | 8 | final class ConnectionProvider(Connection, alias flags) { 9 | private alias ConnectionPool = typeof(this), 10 | Flags = typeof(cast()flags); 11 | 12 | static getInstance(string host, string user, string password, string database, 13 | ushort port = flags ? 3306 : 5432, uint maxConnections = 10, uint initialConnections = 3, 14 | uint incrementalConnections = 3, Duration waitTime = 5.seconds, Flags caps = flags) 15 | in (initialConnections && incrementalConnections) { 16 | if (!_instance) { 17 | synchronized (ConnectionPool.classinfo) { 18 | if (!_instance) 19 | _instance = new ConnectionPool(host, user, password, database, port, 20 | maxConnections, initialConnections, incrementalConnections, waitTime, caps); 21 | } 22 | } 23 | 24 | return _instance; 25 | } 26 | 27 | private this(string host, string user, string password, string database, ushort port, 28 | uint maxConnections, uint initialConnections, uint incrementalConnections, Duration waitTime, 29 | Flags caps) shared { 30 | _pool = cast(shared)spawn(Pool(host, user, password, database, port, 31 | maxConnections, initialConnections, incrementalConnections, waitTime, caps)); 32 | while (!_instantiated) 33 | Thread.sleep(0.msecs); 34 | } 35 | 36 | ~this() shared { 37 | (cast()_pool).send(Terminate(thisTid)); 38 | 39 | L_receive: 40 | try 41 | receive((Terminate _) {}); 42 | catch (OwnerTerminated e) { 43 | if (e.tid != thisTid) 44 | goto L_receive; 45 | } 46 | 47 | _instantiated = false; 48 | } 49 | 50 | Connection getConnection(Duration waitTime = 5.seconds) shared { 51 | (cast()_pool).send(RequestConnection(thisTid)); 52 | Connection conn; 53 | 54 | L_receive: 55 | try { 56 | receiveTimeout( 57 | waitTime, 58 | (shared Connection c) { conn = cast()c; }, 59 | (immutable ConnBusy _) { conn = null; } 60 | ); 61 | } catch (OwnerTerminated e) { 62 | if (e.tid != thisTid) 63 | goto L_receive; 64 | } 65 | 66 | return conn; 67 | } 68 | 69 | void releaseConnection(ref Connection conn) { 70 | enforce(conn.pooled, "This connection is not a managed connection in the pool."); 71 | enforce(!conn.inTransaction, "This connection also has uncommitted or unrollbacked transaction."); 72 | 73 | _pool.send(cast(shared)conn); 74 | conn = null; 75 | } 76 | 77 | private: 78 | __gshared ConnectionPool _instance; 79 | 80 | Tid _pool; 81 | 82 | shared static bool _instantiated; 83 | 84 | static struct Pool { 85 | this(string host, string user, string password, string database, ushort port, 86 | uint maxConnections, uint initialConnections, uint incrementalConnections, Duration waitTime, 87 | Flags caps) { 88 | _host = host; 89 | _user = user; 90 | _pwd = password; 91 | _db = database; 92 | _port = port; 93 | _maxConnections = maxConnections; 94 | _initialConnections = initialConnections; 95 | _incrementalConnections = incrementalConnections; 96 | _waitTime = waitTime; 97 | static if (flags) 98 | _flags = caps; 99 | 100 | createConnections(initialConnections); 101 | _lastShrinkTime = cast(DateTime)Clock.currTime; 102 | _instantiated = true; 103 | } 104 | 105 | void opCall() { 106 | bool loop = true; 107 | 108 | while (loop) { 109 | try { 110 | receive( 111 | (RequestConnection req) { getConnection(req); }, 112 | (shared Connection conn) { releaseConnection(conn); }, 113 | (Terminate t) { 114 | foreach (conn; _pool) 115 | (cast()conn).close(); 116 | 117 | t.tid.send(t); 118 | loop = false; 119 | } 120 | ); 121 | } catch (OwnerTerminated) { 122 | } 123 | 124 | // Shrink the pool. 125 | auto now = cast(DateTime)Clock.currTime; 126 | if (now - _lastShrinkTime > 60.seconds && _pool.length > _initialConnections) { 127 | typeof(_pool) tmp; 128 | foreach (ref conn; _pool) { 129 | if (conn && !(cast()conn).busy && now - conn.releaseTime > 120.seconds) { 130 | try 131 | (cast()conn).close(); 132 | catch (Exception) { 133 | } 134 | conn = null; 135 | } 136 | if (conn) 137 | tmp ~= conn; 138 | } 139 | _pool = tmp; 140 | 141 | _lastShrinkTime = now; 142 | } 143 | } 144 | } 145 | 146 | Connection createConnection() { 147 | try { 148 | static if (flags) 149 | auto conn = new Connection(_host, _user, _pwd, _db, _port, _flags); 150 | else 151 | auto conn = new Connection(_host, _user, _pwd, _db, _port); 152 | conn.pooled = true; 153 | return conn; 154 | } catch (Exception) 155 | return null; 156 | } 157 | 158 | void createConnections(uint n) { 159 | while (n--) { 160 | if (_maxConnections > 0 && _pool.length >= _maxConnections) 161 | break; 162 | 163 | if (auto conn = createConnection()) 164 | _pool ~= cast(shared)conn; 165 | } 166 | } 167 | 168 | void getConnection(RequestConnection req) { 169 | immutable start = Clock.currTime; 170 | 171 | while (true) { 172 | if (auto conn = getFreeConnection()) { 173 | req.tid.send(cast(shared)conn); 174 | return; 175 | } 176 | 177 | if (Clock.currTime - start >= _waitTime) 178 | break; 179 | 180 | Thread.sleep(100.msecs); 181 | } 182 | 183 | req.tid.send(ConnectionBusy); 184 | } 185 | 186 | Connection getFreeConnection() { 187 | Connection conn = findFreeConnection(); 188 | 189 | if (conn is null) { 190 | createConnections(_incrementalConnections); 191 | conn = findFreeConnection(); 192 | } 193 | 194 | return conn; 195 | } 196 | 197 | Connection findFreeConnection() { 198 | Connection result; 199 | 200 | foreach (conn; cast(Connection[])_pool) { 201 | if (conn is null || conn.busy) 202 | continue; 203 | 204 | if (testConnection(conn)) { 205 | conn.busy = true; 206 | result = conn; 207 | break; 208 | } 209 | } 210 | 211 | typeof(_pool) tmp; 212 | foreach (conn; _pool) { 213 | if (conn) 214 | tmp ~= conn; 215 | } 216 | _pool = tmp; 217 | 218 | return result; 219 | } 220 | 221 | bool testConnection(Connection conn) nothrow { 222 | try { 223 | conn.ping(); 224 | return true; 225 | } catch (Exception) { 226 | conn.close(); 227 | conn = null; 228 | 229 | return false; 230 | } 231 | } 232 | 233 | void releaseConnection(shared Connection conn) { 234 | (cast()conn).busy = false; 235 | conn.releaseTime = cast(DateTime)Clock.currTime; 236 | } 237 | 238 | shared(Connection)[] _pool; 239 | 240 | string _host; 241 | string _user; 242 | string _pwd; 243 | string _db; 244 | ushort _port; 245 | uint _maxConnections; 246 | uint _initialConnections; 247 | uint _incrementalConnections; 248 | Duration _waitTime; 249 | static if (flags) 250 | Flags _flags; 251 | 252 | DateTime _lastShrinkTime; 253 | } 254 | } 255 | 256 | private: 257 | 258 | struct RequestConnection { 259 | Tid tid; 260 | } 261 | 262 | immutable ConnectionBusy = ConnBusy(); 263 | 264 | struct ConnBusy { 265 | } 266 | 267 | struct Terminate { 268 | Tid tid; 269 | } 270 | 271 | /+ 272 | unittest { 273 | import core.thread; 274 | import std.stdio; 275 | 276 | auto pool = ConnectionPool.getInstance("127.0.0.1", "root", "111111", "test", 3306); 277 | 278 | foreach (i; 0 .. 20) { 279 | Thread.sleep(100.msecs); 280 | Connection conn = pool.getConnection(); 281 | if (conn) { 282 | writeln(conn.connected()); 283 | pool.releaseConnection(conn); 284 | } 285 | } 286 | pool.destroy(); 287 | } 288 | +/ 289 | -------------------------------------------------------------------------------- /database/postgresql/connection.d: -------------------------------------------------------------------------------- 1 | module database.postgresql.connection; 2 | 3 | import database.postgresql.db, 4 | database.postgresql.packet, 5 | database.postgresql.protocol, 6 | database.postgresql.row, 7 | database.postgresql.type, 8 | database.util; 9 | 10 | alias Socket = DBSocket!PgSQLConnectionException; 11 | 12 | struct Status { 13 | bool ready; 14 | TransactionStatus transaction = TransactionStatus.Idle; 15 | 16 | ulong affected, insertID; 17 | } 18 | 19 | // dfmt off 20 | struct Settings { 21 | string 22 | host, 23 | user, 24 | pwd, 25 | db; 26 | ushort port = 5432; 27 | } 28 | 29 | private struct ServerInfo { 30 | string 31 | versionStr, 32 | encoding, 33 | application, 34 | timeZone; 35 | 36 | uint processId, 37 | cancellationKey; 38 | } 39 | // dfmt on 40 | 41 | @safe: 42 | class Connection { 43 | import std.datetime, 44 | std.functional, 45 | std.logger, 46 | std.range, 47 | std.string, 48 | std.traits, 49 | std.conv : text; 50 | 51 | Socket socket; 52 | 53 | this(Settings settings) { 54 | settings_ = settings; 55 | connect(); 56 | } 57 | 58 | this(string host, string user, string pwd, string db, ushort port = 5432) { 59 | this(Settings(host, user, pwd, db, port)); 60 | } 61 | 62 | void ping() { 63 | } 64 | 65 | alias OnDisconnectCallback = void delegate(); 66 | 67 | @property final { 68 | bool inTransaction() const => connected && status_.transaction == TransactionStatus.Inside; 69 | 70 | ulong insertID() const nothrow @nogc => status_.insertID; 71 | 72 | ulong affected() const nothrow @nogc => status_.affected; 73 | 74 | bool ready() const nothrow @nogc => status_.ready; 75 | 76 | bool connected() const => socket && socket.isAlive; 77 | 78 | auto settings() const => settings_; 79 | 80 | auto notices() const => notices_; 81 | 82 | auto notifications() const => notifications_; 83 | } 84 | 85 | auto runSql(T = PgSQLRow)(in char[] sql) @trusted { 86 | ensureConnected(); 87 | 88 | auto len = 5 + sql.length + 1; 89 | mixin Output!(len, OMT.Query); 90 | op.put(sql); 91 | socket.write(op.data); 92 | return QueryResult!T(this); 93 | } 94 | 95 | auto query(T = PgSQLRow, Args...)(in char[] sql, auto ref Args args) { 96 | prepare!Args("", sql); 97 | bind("", "", forward!args); 98 | flush(); 99 | eatStatuses(IMT.BindComplete, true); 100 | describe(); 101 | sendExecute(); 102 | return QueryResult!T(this, FormatCode.Binary); 103 | } 104 | 105 | ulong exec(Args...)(in char[] sql, auto ref Args args) { 106 | prepare!Args("", sql); 107 | bind("", "", forward!args); 108 | flush(); 109 | eatStatuses(IMT.BindComplete, true); 110 | sendExecute(); 111 | eatStatuses(); 112 | return affected; 113 | } 114 | 115 | void prepare(Args...)(in char[] statement, in char[] sql)@trusted 116 | if (Args.length <= short.max) { 117 | if (statement.length > 255) 118 | throw new PgSQLException("statement name is too long"); 119 | ensureConnected(); 120 | 121 | auto len = 5 + 122 | statement.length + 1 + 123 | sql.length + 1 + 124 | 2 + 125 | Args.length * 4; 126 | mixin Output!(len, OMT.Parse); 127 | op.put(statement); 128 | op.put(sql); 129 | op.put!short(Args.length); 130 | foreach (T; Args) 131 | op.put(PgTypeof!T); 132 | socket.write(op.data); 133 | } 134 | 135 | void bind(Args...)(in char[] portal, in char[] statement, auto ref Args args) @trusted 136 | if (Args.length <= short.max) { 137 | auto len = 5 + 138 | portal.length + 1 + 139 | statement.length + 1 + 140 | 2 + 4 + 141 | 4 * Args.length + 142 | 4; 143 | foreach (i, arg; args) { 144 | enum PT = PgTypeof!(Args[i]); 145 | static assert(PT != PgType.UNKNOWN, "Unrecognized type " ~ Args[i].stringof); 146 | static if (isArray!(Args[i])) { 147 | len += arg.length; 148 | } else { 149 | len += PT == PgType.TIMESTAMPTZ ? 4 : arg.sizeof; 150 | } 151 | } 152 | if (len >= int.max) 153 | throw new PgSQLException("bind message is too long"); 154 | mixin Output!(len, OMT.Bind); 155 | op.put(portal); 156 | op.put(statement); 157 | op.put!short(1); 158 | op.put(FormatCode.Binary); 159 | op.put!short(Args.length); 160 | foreach (i, arg; args) { 161 | enum PT = PgTypeof!(Args[i]); 162 | static if (PT == PgType.NULL) 163 | op.put(-1); 164 | else static if (isArray!(Args[i])) { 165 | op.put(cast(int)arg.length); 166 | op.put(cast(ubyte[])arg); 167 | } else { 168 | enum int size = PT == PgType.TIMESTAMPTZ ? 4 : arg.sizeof; 169 | op.put(size); 170 | op.put(arg); 171 | } 172 | } 173 | op.put!short(1); 174 | op.put(FormatCode.Binary); 175 | socket.write(op.data); 176 | } 177 | 178 | void describe(in char[] name = "", DescribeType type = DescribeType.Statement) @trusted { 179 | auto len = 5 + 1 + name.length + 1; 180 | mixin Output!(len, OMT.Describe); 181 | op.put(type); 182 | op.put(name); 183 | socket.write(op.data); 184 | } 185 | 186 | void flush() { 187 | enum ubyte[5] buf = [OMT.Flush, 0, 0, 0, 4]; 188 | socket.write(buf); 189 | } 190 | 191 | void sync() { 192 | enum ubyte[5] buf = [OMT.Sync, 0, 0, 0, 4]; 193 | socket.write(buf); 194 | } 195 | 196 | ulong executePortal(string portal = "", int rowLimit = 0) { 197 | sendExecute(portal, rowLimit); 198 | eatStatuses(); 199 | return affected; 200 | } 201 | 202 | void cancel(uint processId, uint cancellationKey) @trusted { 203 | ubyte[16] buf = [16, 0, 0, 0, 4, 210, 22, 46, 0, 0, 0, 0, 0, 0, 0, 0]; 204 | *cast(uint*)&buf[8] = native(processId); 205 | *cast(uint*)&buf[12] = native(cancellationKey); 206 | socket.write(buf); 207 | } 208 | 209 | void close(DescribeType type, in char[] name = "") @trusted { 210 | auto len = 5 + 1 + name.length + 1; 211 | mixin Output!(len, OMT.Close); 212 | op.put(type); 213 | op.put(name); 214 | socket.write(op.data); 215 | flush(); 216 | eatStatuses(IMT.CloseComplete); 217 | } 218 | 219 | bool begin() { 220 | if (inTransaction) 221 | throw new PgSQLErrorException( 222 | "PgSQL doesn't support nested transactions" ~ 223 | "- commit or rollback before starting a new transaction"); 224 | 225 | query("begin"); 226 | return inTransaction; 227 | } 228 | 229 | bool commit() { 230 | if (!inTransaction) 231 | throw new PgSQLErrorException("No active transaction"); 232 | 233 | query("commit"); 234 | return !inTransaction; 235 | } 236 | 237 | bool rollback() { 238 | if (!inTransaction) 239 | throw new PgSQLErrorException("No active transaction"); 240 | 241 | query("rollback"); 242 | return !inTransaction; 243 | } 244 | 245 | void close(bool sendTerminate = true) nothrow { 246 | scope (exit) { 247 | socket.close(); 248 | socket = null; 249 | } 250 | enum ubyte[5] terminateMsg = [OMT.Terminate, 0, 0, 0, 4]; 251 | if (sendTerminate) 252 | try 253 | socket.write(terminateMsg); 254 | catch (Exception) { 255 | } 256 | } 257 | 258 | void reuse() { 259 | onDisconnect = null; 260 | ensureConnected(); 261 | 262 | if (inTransaction) 263 | rollback(); 264 | } 265 | 266 | package(database): 267 | 268 | bool busy, pooled; 269 | DateTime releaseTime; 270 | 271 | private: 272 | void disconnect() { 273 | close(false); 274 | if (onDisconnect) 275 | onDisconnect(); 276 | } 277 | 278 | void connect() @trusted { 279 | socket = new Socket(settings_.host, settings_.port); 280 | 281 | auto len = 5 + 4 + 282 | "user".length + 1 + settings_.user.length + 1 + 283 | (settings_.db.length ? "database".length + 1 + settings_.db.length + 1 : 0); 284 | mixin Output!len; 285 | alias startup = op; 286 | startup.put(0x00030000u); 287 | startup.put("user"); 288 | startup.put(settings_.user); 289 | if (settings_.db.length) { 290 | startup.put("database"); 291 | startup.put(settings_.db); 292 | } 293 | startup.put!ubyte(0); 294 | socket.write(startup.data); 295 | 296 | if (eatAuth()) 297 | eatAuth(); 298 | eatBackendKeyData(eatStatuses(IMT.BackendKeyData)); 299 | eatStatuses(); 300 | } 301 | 302 | void sendExecute(string portal = "", int rowLimit = 0) @trusted { 303 | auto len = 5 + portal.length + 1 + 4; 304 | mixin Output!(len, OMT.Execute); 305 | op.put(portal); 306 | op.put(rowLimit); 307 | socket.write(op.data); 308 | sync(); 309 | } 310 | 311 | void ensureConnected() { 312 | if (!connected) 313 | connect(); 314 | } 315 | 316 | InputPacket retrieve(ubyte control) @trusted { 317 | scope (failure) 318 | disconnect(); 319 | 320 | uint[1] header = void; 321 | socket.read(header); 322 | 323 | auto len = native(header[0]) - 4; 324 | buf.length = len; 325 | socket.read(buf); 326 | 327 | if (buf.length != len) 328 | throw new PgSQLConnectionException("Wrong number of bytes read"); 329 | 330 | return InputPacket(control, buf); 331 | } 332 | 333 | package InputPacket retrieve() @trusted { 334 | scope (failure) 335 | disconnect(); 336 | 337 | ubyte[5] header = void; 338 | socket.read(header); 339 | 340 | uint len = native(*cast(uint*)&header[1]) - 4; 341 | buf.length = len; 342 | socket.read(buf); 343 | 344 | if (buf.length != len) 345 | throw new PgSQLConnectionException("Wrong number of bytes read"); 346 | 347 | return InputPacket(header[0], buf); 348 | } 349 | 350 | package void eatStatuses() @trusted { 351 | InputPacket packet = void; 352 | do 353 | packet = retrieve(); 354 | while (eatStatus(packet) != IMT.ReadyForQuery); 355 | } 356 | 357 | package auto eatStatuses(IMT type, bool syncOnError = false) @trusted { 358 | InputPacket packet = void; 359 | do { 360 | packet = retrieve(); 361 | if (packet.type == type) 362 | break; 363 | } 364 | while (eatStatus(packet, syncOnError) != IMT.ReadyForQuery); 365 | return packet; 366 | } 367 | 368 | bool eatAuth() @trusted { 369 | import std.algorithm : max; 370 | 371 | scope (failure) 372 | disconnect(); 373 | 374 | auto packet = retrieve(); 375 | auto type = cast(IMT)packet.type; 376 | switch (type) with (IMT) { 377 | case Authentication: 378 | auto auth = packet.eat!uint; 379 | version (NoMD5Auth) 380 | auto len = 5 + settings_.pwd.length + 1; 381 | else 382 | auto len = 5 + max(settings_.pwd.length, 3 + 32) + 1; // 3 for md5 and 32 is hash size 383 | mixin Output!(len, OMT.PasswordMessage); 384 | switch (auth) { 385 | case 0: 386 | return false; 387 | case 3: 388 | op.put(settings_.pwd); 389 | break; 390 | version (NoMD5Auth) { 391 | } else { 392 | case 5: 393 | static char[32] MD5toHex(T...)(in T data) { 394 | import std.ascii : LetterCase; 395 | import std.digest.md : md5Of, toHexString; 396 | 397 | return md5Of(data).toHexString!(LetterCase.lower); 398 | } 399 | 400 | auto salt = packet.eat!(ubyte[])(4); 401 | op.put("md5".representation); 402 | op.put(MD5toHex(MD5toHex(settings_.pwd, settings_.user), salt)); 403 | break; 404 | } 405 | /+case 6: // SCM 406 | case 7: // GSS 407 | case 8: 408 | case 9: 409 | case 10: // SASL 410 | case 11: 411 | case 12:+/ 412 | default: 413 | throw new PgSQLProtocolException(text("Unsupported authentication method: ", auth)); 414 | } 415 | 416 | socket.write(op.data); 417 | break; 418 | case NoticeResponse: 419 | eatNoticeResponse(packet); 420 | break; 421 | case ErrorResponse: 422 | eatNoticeResponse(packet); 423 | throwErr(); 424 | default: 425 | throw new PgSQLProtocolException(text("Unexpected message: ", type)); 426 | } 427 | 428 | return true; 429 | } 430 | 431 | void eatParameterStatus(ref InputPacket packet) 432 | in (packet.type == IMT.ParameterStatus) 433 | out (; packet.empty) { 434 | auto name = packet.eatz(); 435 | auto value = packet.eatz(); 436 | //info("parameter ", name, " = ", value); 437 | // dfmt off 438 | switch (hashOf(name)) { 439 | case hashOf("server_version"): server.versionStr = value.dup; break; 440 | case hashOf("server_encoding"): server.encoding = value.dup; break; 441 | case hashOf("application_name"): server.application = value.dup; break; 442 | case hashOf("TimeZone"): server.timeZone = value.dup; break; 443 | default: 444 | } 445 | // dfmt on 446 | } 447 | 448 | void eatBackendKeyData(InputPacket packet) 449 | in (packet.type == IMT.BackendKeyData) { 450 | server.processId = packet.eat!uint; 451 | server.cancellationKey = packet.eat!uint; 452 | } 453 | 454 | void eatNoticeResponse(ref InputPacket packet) 455 | in (packet.type == IMT.NoticeResponse || packet.type == IMT 456 | .ErrorResponse) { 457 | Notice notice; 458 | auto field = packet.eat!ubyte; 459 | // dfmt off 460 | while (field) { 461 | auto value = packet.eatz(); 462 | switch (field) with (NoticeMessageField) { 463 | case Severity: 464 | case SeverityLocal: 465 | switch (hashOf(value)) with (Notice.Severity) { 466 | case hashOf("ERROR"): notice.severity = ERROR; break; 467 | case hashOf("FATAL"): notice.severity = FATAL; break; 468 | case hashOf("PANIC"): notice.severity = PANIC; break; 469 | case hashOf("WARNING"): notice.severity = WARNING; break; 470 | case hashOf("DEBUG"): notice.severity = DEBUG; break; 471 | case hashOf("INFO"): notice.severity = INFO; break; 472 | case hashOf("LOG"): notice.severity = LOG; break; 473 | default: 474 | } 475 | break; 476 | case Code: notice.code = value; break; 477 | case Message: notice.message = value.idup; break; 478 | case Detail: notice.detail = value.idup; break; 479 | case Hint: notice.hint = value.idup; break; 480 | case Position: notice.position = value.parse!uint; break; 481 | case InternalPosition: notice.internalPos = value.idup; break; 482 | case InternalQuery: notice.internalQuery = value.idup; break; 483 | case Where: notice.where = value.idup; break; 484 | case Schema: notice.schema = value.idup; break; 485 | case Table: notice.table = value.idup; break; 486 | case Column: notice.column = value.idup; break; 487 | case DataType: notice.type = value.idup; break; 488 | case Constraint: notice.constraint = value.idup; break; 489 | case File: notice.file = value.idup; break; 490 | case Line: notice.line = value.idup; break; 491 | case Routine: notice.routine = value.idup; break; 492 | default: 493 | info("pgsql notice: ", cast(char)field, ' ', value); 494 | } 495 | field = packet.eat!ubyte; 496 | } 497 | // dfmt on 498 | notices_ ~= notice; 499 | } 500 | 501 | void eatNotification(ref InputPacket packet) 502 | in (packet.type == IMT.NotificationResponse) { 503 | notifications_ ~= Notification(packet.eat!int, packet.eatz(), packet.eatz()); 504 | } 505 | 506 | void eatCommandComplete(ref InputPacket packet) 507 | in (packet.type == IMT.CommandComplete) { 508 | import std.algorithm : swap; 509 | 510 | auto tag = packet.eatz(); 511 | auto p = tag.indexOf(' ') + 1; 512 | auto cmd = tag[0 .. p]; 513 | if (p) 514 | tag = tag[p .. $]; 515 | else 516 | swap(tag, cmd); 517 | 518 | switch (hashOf(cmd)) { 519 | case hashOf("INSERT"): 520 | status_.insertID = tag.parse!ulong; 521 | status_.affected = tag.parse!ulong(1); 522 | break; 523 | case hashOf("SELECT"), hashOf("DELETE"), hashOf("UPDATE"), 524 | hashOf("MOVE"), hashOf("FETCH"), hashOf("COPY"): 525 | status_.insertID = 0; 526 | status_.affected = tag.parse!ulong; 527 | break; 528 | case hashOf("CREATE"), hashOf("DROP"): 529 | status_.insertID = 0; 530 | status_.affected = 0; 531 | break; 532 | default: 533 | } 534 | } 535 | 536 | auto eatStatus(ref InputPacket packet, bool syncOnError = false) { 537 | IMT type = cast(IMT)packet.type; 538 | switch (type) with (IMT) { 539 | case ParameterStatus: 540 | eatParameterStatus(packet); 541 | break; 542 | case ReadyForQuery: 543 | notices_.length = 0; 544 | status_.transaction = packet.eat!TransactionStatus; 545 | status_.ready = true; 546 | break; 547 | case NoticeResponse: 548 | eatNoticeResponse(packet); 549 | break; 550 | case ErrorResponse: 551 | if (syncOnError) 552 | sync(); 553 | eatNoticeResponse(packet); 554 | throwErr(); 555 | case NotificationResponse: 556 | eatNotification(packet); 557 | break; 558 | case CommandComplete: 559 | eatCommandComplete(packet); 560 | break; 561 | case EmptyQueryResponse, NoData, ParameterDescription, 562 | ParseComplete, BindComplete, PortalSuspended: 563 | break; 564 | default: 565 | throw new PgSQLProtocolException(text("Unexpected message: ", type)); 566 | } 567 | return type; 568 | } 569 | 570 | noreturn throwErr() { 571 | foreach (ref notice; notices_) switch (notice.severity) with (Notice.Severity) { 572 | case PANIC, ERROR, FATAL: 573 | throw new PgSQLErrorException(notice.message); 574 | default: 575 | } 576 | 577 | throw new PgSQLErrorException(notices_.front.message); 578 | } 579 | 580 | ubyte[] buf; 581 | 582 | OnDisconnectCallback onDisconnect; 583 | Status status_; 584 | Settings settings_; 585 | ServerInfo server; 586 | Notice[] notices_; 587 | Notification[] notifications_; 588 | } 589 | -------------------------------------------------------------------------------- /database/postgresql/db.d: -------------------------------------------------------------------------------- 1 | module database.postgresql.db; 2 | 3 | import database.postgresql.connection, 4 | database.postgresql.packet, 5 | database.postgresql.protocol, 6 | database.postgresql.row, 7 | database.postgresql.type, 8 | std.traits; 9 | public import database.sqlbuilder; 10 | import std.utf; 11 | 12 | @safe: 13 | 14 | struct PgSQLDB { 15 | Connection conn; 16 | alias conn this; 17 | 18 | this(Settings settings) { 19 | conn = new Connection(settings); 20 | } 21 | 22 | this(string host, string user, string pwd, string db, ushort port = 5432) { 23 | conn = new Connection(host, user, pwd, db, port); 24 | } 25 | 26 | bool create(Tables...)() if (Tables.length) { 27 | import std.array, std.meta; 28 | 29 | static if (Tables.length == 1) 30 | exec(SB.create!Tables); 31 | else { 32 | enum sql = [staticMap!(SB.create, Tables)].join(';'); 33 | runSql(sql); 34 | } 35 | return true; 36 | } 37 | 38 | ulong insert(OR or = OR.None, T)(T s) if (isAggregateType!T) { 39 | mixin getSQLFields!(or ~ "INTO " ~ quote(SQLName!T) ~ '(', 40 | ")VALUES(" ~ placeholders(ColumnCount!T) ~ ')', T); 41 | 42 | enum sql = SB(sql!colNames, State.insert); 43 | return exec(sql, s.tupleof); 44 | } 45 | 46 | ulong replaceInto(T)(T s) => insert!(OR.Replace, T)(s); 47 | 48 | auto selectAllWhere(T, string expr, A...)(auto ref A args) if (expr.length) 49 | => this.query!T(SB.selectAllFrom!T.where(expr), args); 50 | 51 | T selectOneWhere(T, string expr, A...)(auto ref A args) if (expr.length) { 52 | auto q = query(SB.selectAllFrom!T.where(expr), args); 53 | if (q.empty) 54 | throw new PgSQLException("No match"); 55 | return q.get!T; 56 | } 57 | 58 | T selectOneWhere(T, string expr, T defValue, A...)(auto ref A args) 59 | if (expr.length) { 60 | auto q = query(SB.selectAllFrom!T.where(expr), args); 61 | return q ? q.get!T : defValue; 62 | } 63 | 64 | bool hasTable(string table) 65 | => !query("select 1 from pg_class where relname = $1", table).empty; 66 | 67 | bool hasTable(T)() if (isAggregateType!T) { 68 | enum sql = "select 1 from pg_class where relname = " ~ quote(SQLName!T); 69 | return !query(sql).empty; 70 | } 71 | 72 | long delWhere(T, string expr, A...)(auto ref A args) if (expr.length) { 73 | enum sql = SB.del!T.where(expr); 74 | return exec(sql, args); 75 | } 76 | } 77 | 78 | struct QueryResult(T = PgSQLRow) { 79 | Connection connection; 80 | alias connection this; 81 | PgSQLRow row; 82 | @disable this(); 83 | 84 | this(Connection conn, FormatCode format = FormatCode.Text) { 85 | connection = conn; 86 | auto packet = eatStatuses(InputMessageType.RowDescription); 87 | if (packet.type == InputMessageType.ReadyForQuery) { 88 | row.values.length = 0; 89 | return; 90 | } 91 | auto columns = packet.eat!ushort; 92 | row.header = PgSQLHeader(columns, packet); 93 | foreach (ref col; row.header) 94 | col.format = format; 95 | popFront(); 96 | } 97 | 98 | ~this() { 99 | clear(); 100 | } 101 | 102 | @property pure nothrow @nogc { 103 | bool empty() const => row.values.length == 0; 104 | 105 | PgSQLHeader header() => row.header; 106 | 107 | T opCast(T : bool)() const => !empty; 108 | } 109 | 110 | void popFront() { 111 | auto packet = eatStatuses(InputMessageType.DataRow); 112 | if (packet.type == InputMessageType.ReadyForQuery) { 113 | row.values.length = 0; 114 | return; 115 | } 116 | const rowlen = packet.eat!ushort; 117 | foreach (i, ref column; row.header) 118 | if (i < rowlen) 119 | row[i] = eatValue(packet, column); 120 | else 121 | row[i] = PgSQLValue(null); 122 | assert(packet.empty); 123 | } 124 | 125 | T front() { 126 | static if (is(Unqual!T == PgSQLRow)) 127 | return row; 128 | else 129 | return get(); 130 | } 131 | 132 | U get(U = T)() { 133 | static if (isAggregateType!U) 134 | return row.get!U; 135 | else 136 | return row[0].get!U; 137 | } 138 | 139 | U peek(U = T)() { 140 | static if (isAggregateType!U) 141 | return row.get!U; 142 | else 143 | return row[0].peek!U; 144 | } 145 | 146 | void clear() { 147 | if (!empty && !connection.ready) { 148 | eatStatuses(); 149 | row.values.length = 0; 150 | } 151 | } 152 | } 153 | 154 | private: 155 | alias SB = SQLBuilder; 156 | 157 | PgSQLValue eatValue(ref InputPacket packet, in PgSQLColumn column) { 158 | import std.array; 159 | import std.conv : to; 160 | import std.datetime; 161 | 162 | auto length = packet.eat!uint; 163 | if (length == uint.max) 164 | return PgSQLValue(null); 165 | 166 | if (column.format == FormatCode.Binary) { 167 | switch (column.type) with (PgType) { 168 | case BOOL: 169 | return PgSQLValue(packet.eat!bool); 170 | case CHAR: 171 | return PgSQLValue(packet.eat!char); 172 | case INT2: 173 | return PgSQLValue(packet.eat!short); 174 | case INT4: 175 | return PgSQLValue(packet.eat!int); 176 | case INT8: 177 | return PgSQLValue(packet.eat!long); 178 | case REAL: 179 | return PgSQLValue(packet.eat!float); 180 | case DOUBLE: 181 | return PgSQLValue(packet.eat!double); 182 | case VARCHAR, CHARA, 183 | TEXT, NAME: 184 | return PgSQLValue(column.type, packet.eat!(char[])(length).dup); 185 | case BYTEA: 186 | return PgSQLValue(packet.eat!(ubyte[])(length).dup); 187 | case DATE: 188 | return PgSQLValue(packet.eat!Date); 189 | case TIME: 190 | return PgSQLValue(packet.eat!TimeOfDay); 191 | case TIMESTAMP: 192 | return PgSQLValue(packet.eat!DateTime); 193 | case TIMESTAMPTZ: 194 | return PgSQLValue(packet.eat!SysTime); 195 | default: 196 | } 197 | throw new PgSQLErrorException("Unsupported type " ~ column.type.columnTypeName); 198 | } 199 | auto svalue = packet.eat!(const(char)[])(length); 200 | switch (column.type) with (PgType) { 201 | case UNKNOWN, NULL: 202 | return PgSQLValue(null); 203 | case BOOL: 204 | return PgSQLValue(svalue[0] == 't'); 205 | case CHAR: 206 | return PgSQLValue(svalue[0]); 207 | case INT2: 208 | return PgSQLValue(svalue.to!short); 209 | case INT4: 210 | return PgSQLValue(svalue.to!int); 211 | case INT8: 212 | return PgSQLValue(svalue.to!long); 213 | case REAL: 214 | return PgSQLValue(svalue.to!float); 215 | case DOUBLE: 216 | return PgSQLValue(svalue.to!double); 217 | 218 | case NUMERIC, MONEY, 219 | BIT, VARBIT, 220 | INET, CIDR, MACADDR, MACADDR8, 221 | UUID, JSON, XML, 222 | TEXT, NAME, 223 | VARCHAR, CHARA: 224 | return PgSQLValue(column.type, svalue.dup); 225 | case BYTEA: 226 | if (svalue.length >= 2) 227 | svalue = svalue[2 .. $]; 228 | auto data = uninitializedArray!(ubyte[])(svalue.length >> 1); 229 | foreach (i; 0 .. data.length) 230 | data[i] = cast(ubyte)(hexDecode(svalue[i << 1]) << 4 | hexDecode(svalue[i << 1 | 1])); 231 | return PgSQLValue(data); 232 | case DATE: 233 | return PgSQLValue(parseDate(svalue)); 234 | case TIME, TIMETZ: 235 | return PgSQLValue(parsePgSQLTime(svalue)); 236 | case TIMESTAMP, TIMESTAMPTZ: 237 | return PgSQLValue(parsePgSQLTimestamp(svalue)); 238 | default: 239 | } 240 | throw new PgSQLErrorException("Unsupported type " ~ column.type.columnTypeName); 241 | } 242 | 243 | uint hexDecode(char c) @nogc pure nothrow 244 | => c + 9 * (c >> 6) & 15; 245 | 246 | public unittest { 247 | import database.util; 248 | import std.stdio; 249 | 250 | @snakeCase struct PlaceOwner { 251 | @snakeCase: 252 | @sqlkey() uint placeID; // matches place_id 253 | float locationId; // matches location_id 254 | string ownerName; // matches owner_name 255 | string feedURL; // matches feed_url 256 | } 257 | 258 | auto db = PgSQLDB("127.0.0.1", "postgres", "postgres", "postgres"); 259 | db.runSql(`CREATE TABLE IF NOT EXISTS company( 260 | ID INT PRIMARY KEY NOT NULL, 261 | NAME TEXT NOT NULL, 262 | AGE INT NOT NULL, 263 | ADDRESS CHAR(50), 264 | SALARY REAL, 265 | JOIN_DATE DATE 266 | );`); 267 | assert(db.hasTable("company")); 268 | assert(db.query("select 42").get!int == 42); 269 | db.create!PlaceOwner; 270 | db.insert(PlaceOwner(1, 1, "foo", "")); 271 | db.insert(PlaceOwner(2, 1, "bar", "")); 272 | db.insert(PlaceOwner(3, 7.5, "baz", "")); 273 | auto s = db.selectOneWhere!(PlaceOwner, "owner_name=$1")("bar"); 274 | assert(s.placeID == 2); 275 | assert(s.ownerName == "bar"); 276 | uint count; 277 | foreach (row; db.selectAllWhere!(PlaceOwner, "location_id=$1")(1)) { 278 | writeln(row); 279 | count++; 280 | } 281 | db.exec("drop table company"); 282 | db.exec("drop table place_owner"); 283 | assert(count == 2); 284 | db.close(); 285 | } 286 | -------------------------------------------------------------------------------- /database/postgresql/exception.d: -------------------------------------------------------------------------------- 1 | module database.postgresql.exception; 2 | 3 | import database.util : DBException; 4 | 5 | @safe: 6 | 7 | class PgSQLException : DBException { 8 | this(string msg, string file = __FILE__, size_t line = __LINE__) pure { 9 | super(msg, file, line); 10 | } 11 | } 12 | 13 | class PgSQLConnectionException : PgSQLException { 14 | this(string msg, string file = __FILE__, size_t line = __LINE__) pure { 15 | super(msg, file, line); 16 | } 17 | } 18 | 19 | class PgSQLProtocolException : PgSQLException { 20 | this(string msg, string file = __FILE__, size_t line = __LINE__) pure { 21 | super(msg, file, line); 22 | } 23 | } 24 | 25 | class PgSQLErrorException : DBException { 26 | this(string msg, string file = __FILE__, size_t line = __LINE__) pure { 27 | super(msg, file, line); 28 | } 29 | } 30 | 31 | class PgSQLDuplicateEntryException : PgSQLErrorException { 32 | this(string msg, string file = __FILE__, size_t line = __LINE__) pure { 33 | super(msg, file, line); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /database/postgresql/package.d: -------------------------------------------------------------------------------- 1 | module database.postgresql; 2 | 3 | public import 4 | database.postgresql.db, 5 | database.postgresql.exception, 6 | database.postgresql.pool, 7 | database.postgresql.protocol, 8 | database.postgresql.row, 9 | database.postgresql.type; 10 | -------------------------------------------------------------------------------- /database/postgresql/packet.d: -------------------------------------------------------------------------------- 1 | module database.postgresql.packet; 2 | 3 | import core.stdc.stdlib, 4 | database.postgresql.protocol, 5 | database.util, 6 | std.algorithm, 7 | std.datetime, 8 | std.meta, 9 | std.traits; 10 | 11 | package import database.postgresql.exception; 12 | 13 | @safe @nogc: 14 | 15 | struct InputPacket { 16 | @disable this(); 17 | @disable this(this); 18 | 19 | this(ubyte type, ubyte[] buffer) { 20 | typ = type; 21 | buf = buffer; 22 | } 23 | 24 | @property auto type() const => typ; 25 | 26 | T peek(T)() @trusted const if (!isAggregateType!T && !isArray!T) 27 | in (T.sizeof <= buf.length) => native(cast(const T*)buf.ptr); 28 | 29 | T eat(T)() @trusted if (!isAggregateType!T && !isArray!T) 30 | in (T.sizeof <= buf.length) { 31 | auto p = *cast(T*)buf.ptr; 32 | buf = buf[T.sizeof .. $]; 33 | return native(p); 34 | } 35 | 36 | T eat(T : Date)() => PGEpochDate + dur!"days"(eat!int); 37 | 38 | T eat(T : TimeOfDay)() => PGEpochTime + dur!"usecs"(eat!long); 39 | 40 | T eat(T : DateTime)() // timestamp 41 | => PGEpochDateTime + dur!"usecs"(eat!long); 42 | 43 | T eat(T : SysTime)() // timestamptz 44 | => T(PGEpochDateTime + dur!"usecs"(eat!long)); 45 | 46 | auto eatz() @trusted { 47 | import core.stdc.string; 48 | 49 | auto len = strlen(cast(char*)buf.ptr); 50 | auto result = cast(char[])buf[0 .. len]; 51 | buf = buf[len + 1 .. $]; 52 | return result; 53 | } 54 | 55 | T eat(T : U[], U)(size_t count) @trusted { 56 | assert(U.sizeof * count <= buf.length); 57 | auto ptr = cast(U*)buf.ptr; 58 | buf = buf[U.sizeof * count .. $]; 59 | return ptr[0 .. count]; 60 | } 61 | 62 | mixin InputPacketMethods!PgSQLProtocolException; 63 | 64 | private: 65 | ubyte[] buf; 66 | ubyte typ; 67 | } 68 | 69 | struct OutputPacket { 70 | @disable this(); 71 | @disable this(this); 72 | 73 | this(ubyte[] buffer) 74 | in (buffer.length >= 4) { 75 | if (!buffer) 76 | throw new PgSQLException("Out of memory"); 77 | buf = buffer; 78 | implicit = 4; 79 | } 80 | 81 | this(ubyte type, ubyte[] buffer) 82 | in (buffer.length >= 5) { 83 | if (!buffer) 84 | throw new PgSQLException("Out of memory"); 85 | buf = buffer; 86 | implicit = 5; 87 | buffer[0] = type; 88 | } 89 | 90 | ~this() @trusted { 91 | if (buf.length > LargePacketSize) 92 | free(buf.ptr); 93 | } 94 | 95 | void put(in char[] x) { 96 | put(cast(const ubyte[])x); 97 | put!ubyte(0); 98 | } 99 | 100 | void put(T)(in T[] x) @trusted if (!is(T[] : const char[])) { 101 | check(T.sizeof * x.length); 102 | auto p = cast(T*)(buf.ptr + implicit + pos); 103 | static if (T.sizeof == 1) 104 | p[0 .. x.length] = x; 105 | else { 106 | foreach (y; x) 107 | *p++ = native(y); 108 | } 109 | pos += cast(uint)(T.sizeof * x.length); 110 | } 111 | 112 | pragma(inline, true) void put(T)(T x) @trusted 113 | if (!isAggregateType!T && !isArray!T) { 114 | check(T.sizeof); 115 | *cast(Unqual!T*)(buf.ptr + implicit + pos) = native(x); 116 | pos += T.sizeof; 117 | } 118 | 119 | void put(Date x) 120 | => put(x.dayOfGregorianCal - PGEpochDay); 121 | 122 | void put(TimeOfDay x) 123 | => put(cast(int)(x - PGEpochTime).total!"usecs"); 124 | 125 | void put(DateTime x) // timestamp 126 | => put(cast(int)(x - PGEpochDateTime).total!"usecs"); 127 | 128 | void put(in SysTime x) // timestamptz 129 | => put(cast(int)(x - SysTime(PGEpochDateTime)).total!"usecs"); 130 | 131 | ubyte[] data() @trusted { 132 | check(0); 133 | assert(implicit + pos <= buf.length); 134 | *cast(uint*)(buf.ptr + implicit - 4) = native(pos + 4); 135 | return buf[0 .. implicit + pos]; 136 | } 137 | 138 | mixin OutputPacketMethods; 139 | 140 | private: 141 | void check(size_t size) { 142 | if (implicit + pos + size > int.max) 143 | throw new PgSQLConnectionException("Packet size exceeds 2^31"); 144 | } 145 | 146 | ubyte[] buf; 147 | uint pos, implicit; 148 | } 149 | 150 | package: 151 | 152 | enum LargePacketSize = 32 * 1024; 153 | 154 | alias IMT = InputMessageType, 155 | OMT = OutputMessageType; 156 | 157 | template Output(alias n, Args...) { 158 | import core.stdc.stdlib; 159 | 160 | auto buf = cast(ubyte*)(n > LargePacketSize ? malloc(n) : alloca(n)); 161 | auto op = OutputPacket(Args, buf[0 .. n]); 162 | } 163 | 164 | version (LittleEndian) { 165 | import std.bitmanip : native = swapEndian; 166 | 167 | T native(T)(T value) @trusted if (isFloatingPoint!T) { 168 | union U { 169 | AliasSeq!(int, long)[T.sizeof / 8] i; 170 | T f; 171 | } 172 | 173 | return U(.native(*cast(typeof(U.i)*)&value)).f; 174 | } 175 | } else 176 | T native(T)(in T value) => value; 177 | -------------------------------------------------------------------------------- /database/postgresql/pool.d: -------------------------------------------------------------------------------- 1 | module database.postgresql.pool; 2 | 3 | import database.pool; 4 | import database.postgresql.connection; 5 | 6 | alias ConnectionPool = shared ConnectionProvider!(Connection, 0); 7 | -------------------------------------------------------------------------------- /database/postgresql/protocol.d: -------------------------------------------------------------------------------- 1 | module database.postgresql.protocol; 2 | 3 | import std.datetime; 4 | 5 | //dfmt off 6 | 7 | //https://www.postgresql.org/docs/14/static/protocol-message-formats.html 8 | enum OutputMessageType : ubyte { 9 | Bind = 'B', 10 | Close = 'C', 11 | CopyData = 'd', 12 | CopyDone = 'c', 13 | CopyFail = 'f', 14 | Describe = 'D', 15 | Execute = 'E', 16 | Flush = 'H', 17 | FunctionCall = 'F', 18 | Parse = 'P', 19 | PasswordMessage = 'p', 20 | Query = 'Q', 21 | Sync = 'S', 22 | Terminate = 'X' 23 | } 24 | 25 | enum InputMessageType : ubyte { 26 | Authentication = 'R', 27 | BackendKeyData = 'K', 28 | BindComplete = '2', 29 | CloseComplete = '3', 30 | CommandComplete = 'C', 31 | CopyData = 'd', 32 | CopyDone = 'c', 33 | CopyInResponse = 'G', 34 | CopyOutResponse = 'H', 35 | CopyBothResponse = 'W', 36 | DataRow = 'D', 37 | EmptyQueryResponse = 'I', 38 | ErrorResponse = 'E', 39 | FunctionCallResponse= 'V', 40 | NoData = 'n', 41 | NoticeResponse = 'N', 42 | NotificationResponse= 'A', 43 | ParameterDescription= 't', 44 | ParameterStatus = 'S', 45 | ParseComplete = '1', 46 | PortalSuspended = 's', 47 | ReadyForQuery = 'Z', 48 | RowDescription = 'T' 49 | } 50 | 51 | enum TransactionStatus : ubyte { 52 | Idle = 'I', 53 | Inside = 'T', 54 | Error = 'E', 55 | } 56 | 57 | enum DescribeType : ubyte { 58 | Statement = 'S', 59 | Portal = 'P' 60 | } 61 | 62 | enum FormatCode : short { 63 | Text, 64 | Binary 65 | } 66 | 67 | // https://www.postgresql.org/docs/14/static/protocol-error-fields.html 68 | enum NoticeMessageField : ubyte { 69 | SeverityLocal = 'S', 70 | Severity = 'V', 71 | Code = 'C', 72 | Message = 'M', 73 | Detail = 'D', 74 | Hint = 'H', 75 | Position = 'P', 76 | InternalPosition = 'p', 77 | InternalQuery = 'q', 78 | Where = 'W', 79 | Schema = 's', 80 | Table = 't', 81 | Column = 'c', 82 | DataType = 'd', 83 | Constraint = 'n', 84 | File = 'F', 85 | Line = 'L', 86 | Routine = 'R', 87 | } 88 | 89 | // https://github.com/postgres/postgres/blob/master/src/include/catalog/pg_type.dat 90 | enum PgType : uint { 91 | NULL = 0, 92 | BOOL = 16, 93 | BYTEA = 17, 94 | CHAR = 18, 95 | NAME = 19, 96 | INT8 = 20, 97 | INT2 = 21, 98 | // INTVEC2 = 22, 99 | INT4 = 23, 100 | // REGPROC = 24, 101 | TEXT = 25, 102 | OID = 26, 103 | // TID = 27, 104 | // XID = 28, 105 | // CID = 29, 106 | // OIDARRAY = 30, 107 | 108 | // PG_TYPE = 71, 109 | // PG_ATTRIBUTE = 75, 110 | // PG_PROC = 81, 111 | // PG_CLASS = 83, 112 | 113 | JSON = 114, 114 | XML = 142, 115 | 116 | POINT = 600, 117 | LSEG = 601, 118 | PATH = 602, 119 | BOX = 603, 120 | POLYGON = 604, 121 | LINE = 628, 122 | 123 | REAL = 700, 124 | DOUBLE = 701, 125 | // ABSTIME = 702, 126 | // RELTIME = 703, 127 | TINTERVAL = 704, 128 | UNKNOWN = 705, 129 | CIRCLE = 718, 130 | MONEY = 790, 131 | 132 | MACADDR = 829, 133 | INET = 869, 134 | CIDR = 650, 135 | MACADDR8 = 774, 136 | 137 | CHARA = 1042, 138 | VARCHAR = 1043, 139 | DATE = 1082, 140 | TIME = 1083, 141 | 142 | TIMESTAMP = 1114, 143 | TIMESTAMPTZ = 1184, 144 | INTERVAL = 1186, 145 | 146 | TIMETZ = 1266, 147 | 148 | BIT = 1560, 149 | VARBIT = 1562, 150 | 151 | NUMERIC = 1700, 152 | // REFCURSOR = 1790, 153 | 154 | // REGPROCEDURE = 2202, 155 | // REGOPER = 2203, 156 | // REGOPERATOR = 2204, 157 | // REGCLASS = 2205, 158 | // REGTYPE = 2206, 159 | // REGROLE = 4096, 160 | // REGNAMESPACE = 4089, 161 | VOID = 2278, 162 | UUID = 2950, 163 | JSONB = 3802 164 | } 165 | 166 | alias PgColumnTypes = PgType; 167 | 168 | enum PGEpochDate = Date(2000, 1, 1); 169 | enum PGEpochDay = PGEpochDate.dayOfGregorianCal; 170 | enum PGEpochTime = TimeOfDay(0, 0, 0); 171 | enum PGEpochDateTime = DateTime(2000, 1, 1, 0, 0, 0); 172 | 173 | auto columnTypeName(PgType type) { 174 | import std.traits; 175 | 176 | final switch (type) { 177 | case PgType.DOUBLE: return "double precision"; 178 | case PgType.CHARA: return "char(n)"; 179 | static foreach(M; EnumMembers!PgType) { 180 | static if(M != PgType.DOUBLE && M != PgType.CHARA) 181 | case M: return M.stringof; 182 | } 183 | } 184 | } 185 | 186 | struct Notification { 187 | /// The process ID of the notifying backend process 188 | int processId; 189 | /// The name of the channel that the notify has been raised on 190 | char[] channel; 191 | /// The “payload” string passed from the notifying process 192 | char[] payload; 193 | } 194 | 195 | struct Notice { 196 | enum Severity : ubyte { 197 | ERROR = 1, 198 | FATAL, 199 | PANIC, 200 | WARNING, 201 | NOTICE, 202 | DEBUG, 203 | INFO, 204 | LOG, 205 | } 206 | 207 | Severity severity; 208 | /// https://www.postgresql.org/docs/14/static/errcodes-appendix.html 209 | char[5] code; 210 | /// Primary human-readable error message. This should be accurate 211 | /// but terse (typically one line). Always present. 212 | string message; 213 | /// Optional secondary error message carrying more detail about the 214 | /// problem. Might run to multiple lines. 215 | string detail; 216 | /** Optional suggestion what to do about the problem. This is intended to 217 | differ from Detail in that it offers advice (potentially inappropriate) 218 | rather than hard facts. Might run to multiple lines. */ 219 | string hint; 220 | /** Decimal ASCII integer, indicating an error cursor position as an index 221 | into the original query string. The first character has index 1, and 222 | positions are measured in characters not bytes. */ 223 | uint position; 224 | /** this is defined the same as the position field, but it is used when 225 | the cursor position refers to an internally generated command rather than 226 | the one submitted by the client. The q field will always appear when this 227 | field appears.*/ 228 | string internalPos; 229 | /// Text of a failed internally-generated command. This could be, for 230 | /// example, a SQL query issued by a PL/pgSQL function. 231 | string internalQuery; 232 | /** Context in which the error occurred. Presently this includes a call 233 | stack traceback of active procedural language functions and 234 | internally-generated queries. The trace is one entry per line, most 235 | recent first. */ 236 | string where; 237 | string schema; 238 | string table; 239 | string column; 240 | /// If the error was associated with a specific data type, the name of 241 | /// the data type. 242 | string type; 243 | /** If the error was associated with a specific constraint, the name of the 244 | constraint. Refer to fields listed above for the associated table or domain. 245 | (For this purpose, indexes are treated as constraints, even if they weren't 246 | created with constraint syntax.) */ 247 | string constraint; 248 | /// File name of the source-code location where the error was reported. 249 | string file; 250 | /// Line number of the source-code location where the error was reported. 251 | string line; 252 | /// Name of the source-code routine reporting the error. 253 | string routine; 254 | 255 | string toString() @safe const { 256 | import std.array; 257 | 258 | auto writer = appender!string; 259 | toString(writer); 260 | return writer[]; 261 | } 262 | 263 | void toString(W)(ref W writer) const { 264 | import std.format : formattedWrite; 265 | writer.formattedWrite("%s(%s) %s", severity, code, message); 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /database/postgresql/row.d: -------------------------------------------------------------------------------- 1 | module database.postgresql.row; 2 | 3 | import database.postgresql.exception; 4 | import database.postgresql.type; 5 | public import database.row; 6 | 7 | alias PgSQLRow = Row!(PgSQLValue, PgSQLHeader, PgSQLErrorException, hashOf, Mixin); 8 | 9 | private template Mixin() { 10 | package uint find(size_t hash, string key) const { 11 | if (auto mask = index_.length - 1) { 12 | assert((index_.length & mask) == 0); 13 | 14 | hash &= mask; 15 | uint probe; 16 | 17 | for (;;) { 18 | auto index = index_[hash]; 19 | if (!index) 20 | break; 21 | if (_header[index - 1].name == key) 22 | return index; 23 | hash = (hash + ++probe) & mask; 24 | } 25 | } 26 | return 0; 27 | } 28 | 29 | void structurize(Strict strict = Strict.yesIgnoreNull, string path = null, T)(ref T result) { 30 | import database.postgresql.exception; 31 | import database.postgresql.type; 32 | 33 | foreach (i, ref member; result.tupleof) { 34 | static if (isWritableDataMember!member) { 35 | enum colName = path ~ SQLName!(result.tupleof[i]), 36 | opt = hasUDA!(member, optional) || strict == Strict.no; 37 | 38 | static if (isValueType!(typeof(member))) { 39 | enum hash = colName.hashOf; 40 | 41 | if (const index = find(hash, colName)) { 42 | const pvalue = values[index - 1]; 43 | 44 | static if (strict != Strict.yes || opt) { 45 | if (pvalue.isNull) 46 | continue; 47 | } 48 | 49 | member = pvalue.get!(Unqual!(typeof(member))); 50 | continue; 51 | } 52 | 53 | static if (!opt) 54 | throw new PgSQLErrorException( 55 | "Column '" ~ colName ~ "' was not found in this result set"); 56 | } else 57 | structurize!(opt ? Strict.no : strict, colName ~ '.')(member); 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /database/postgresql/type.d: -------------------------------------------------------------------------------- 1 | module database.postgresql.type; 2 | 3 | import core.bitop : bsr; 4 | import database.postgresql.protocol; 5 | import database.postgresql.packet; 6 | import database.postgresql.row; 7 | import std.conv : text; 8 | import std.datetime; 9 | import std.format : formattedWrite; 10 | import std.traits; 11 | public import database.util; 12 | 13 | enum isValueType(T) = !is(T == struct) || is(Unqual!T == PgSQLValue) || 14 | is(T : Date) || is(T : DateTime) || is(T : SysTime); 15 | 16 | template PgTypeof(T) { 17 | static if (is(T U == enum)) { 18 | static if (is(Unqual!T == PgType)) 19 | enum PgTypeof = PgType.OID; 20 | else 21 | enum PgTypeof = PgTypeof!U; 22 | } else static if (is(T : typeof(null))) 23 | enum PgTypeof = PgType.NULL; 24 | else static if (isIntegral!T) 25 | enum PgTypeof = [PgType.INT2, PgType.INT4, PgType.INT8][T.sizeof / 4]; 26 | else static if (isSomeString!T) 27 | enum PgTypeof = PgType.TEXT; 28 | else static if (isSomeChar!T) 29 | enum PgTypeof = PgType.CHAR; 30 | else { 31 | alias U = Unqual!T; 32 | static if (is(U == float)) 33 | enum PgTypeof = PgType.REAL; 34 | else static if (is(U == double)) 35 | enum PgTypeof = PgType.DOUBLE; 36 | else static if (is(U == Date)) 37 | enum PgTypeof = PgType.DATE; 38 | else static if (is(U == TimeOfDay) || is(U == PgSQLTime)) 39 | enum PgTypeof = PgType.TIME; 40 | else static if (is(U == DateTime) || is(U == PgSQLTimestamp)) 41 | enum PgTypeof = PgType.TIMESTAMP; 42 | else static if (is(U == SysTime)) 43 | enum PgTypeof = PgType.TIMESTAMPTZ; 44 | else static if (is(U : const(ubyte)[]) || is(U : ubyte[n], size_t n)) 45 | enum PgTypeof = PgType.BYTEA; 46 | else 47 | enum PgTypeof = PgType.UNKNOWN; 48 | } 49 | } 50 | 51 | @safe pure: 52 | 53 | struct PgSQLValue { 54 | package this(PgType type, char[] str) { 55 | type_ = type; 56 | arr = cast(ubyte[])str; 57 | } 58 | 59 | this(typeof(null)) { 60 | type_ = PgType.NULL; 61 | } 62 | 63 | this(bool value) { 64 | type_ = PgType.BOOL; 65 | p = value ? 't' : 'f'; 66 | } 67 | 68 | this(T)(T value) @trusted if (isScalarType!T && !isBoolean!T) { 69 | static if (isFloatingPoint!T) { 70 | static assert(T.sizeof <= 8, "Unsupported type: " ~ T.stringof); 71 | enum t = [PgType.REAL, PgType.DOUBLE][T.sizeof / 8]; 72 | } else 73 | enum t = [PgType.CHAR, PgType.INT2, PgType.INT4, PgType.INT8][bsr(T.sizeof)]; 74 | type_ = t; 75 | 76 | *cast(Unqual!T*)&p = value; 77 | } 78 | 79 | this(Date value) @trusted { 80 | type_ = PgType.DATE; 81 | timestamp.date = value; 82 | } 83 | 84 | this(TimeOfDay value) { 85 | this(PgSQLTime(value.hour, value.minute, value.second)); 86 | } 87 | 88 | this(PgSQLTime value) @trusted { 89 | type_ = PgType.TIME; 90 | timestamp.time = value; 91 | } 92 | 93 | this(DateTime value) { 94 | this(PgSQLTimestamp(value.date, PgSQLTime(value.hour, value.minute, value.second))); 95 | } 96 | 97 | this(in SysTime value) @trusted { 98 | this(cast(DateTime)value); 99 | type_ = PgType.TIMESTAMPTZ; 100 | } 101 | 102 | this(in PgSQLTimestamp value) @trusted { 103 | type_ = PgType.TIMESTAMP; 104 | timestamp.date = value.date; 105 | timestamp.time = value.time; 106 | } 107 | 108 | this(const(char)[] value) @trusted { 109 | type_ = PgType.VARCHAR; 110 | arr = cast(ubyte[])value; 111 | } 112 | 113 | this(const(ubyte)[] value) @trusted { 114 | type_ = PgType.BYTEA; 115 | arr = cast(ubyte[])value; 116 | } 117 | 118 | void toString(R)(ref R app) @trusted const { 119 | switch (type_) with (PgType) { 120 | case BOOL: 121 | app.put(*cast(bool*)&p ? "TRUE" : "FALSE"); 122 | break; 123 | case CHAR: 124 | app.formattedWrite("%s", *cast(ubyte*)&p); 125 | break; 126 | case INT2: 127 | app.formattedWrite("%d", *cast(short*)&p); 128 | break; 129 | case INT4: 130 | app.formattedWrite("%d", *cast(int*)&p); 131 | break; 132 | case INT8: 133 | app.formattedWrite("%d", *cast(long*)&p); 134 | break; 135 | case REAL: 136 | app.formattedWrite("%g", *cast(float*)&p); 137 | break; 138 | case DOUBLE: 139 | app.formattedWrite("%g", *cast(double*)&p); 140 | break; 141 | case POINT, LSEG, PATH, BOX, POLYGON, LINE, 142 | TINTERVAL, 143 | CIRCLE, 144 | JSONB, 145 | BYTEA: 146 | app.formattedWrite("%s", arr); 147 | break; 148 | case MONEY, 149 | TEXT, NAME, 150 | BIT, VARBIT, 151 | NUMERIC, 152 | INET, CIDR, MACADDR, MACADDR8, 153 | UUID, JSON, XML, 154 | CHARA, VARCHAR: 155 | app.put(*cast(string*)&p); 156 | break; 157 | case DATE: 158 | timestamp.date.toString(app); 159 | break; 160 | case TIME, TIMETZ: 161 | timestamp.time.toString(app); 162 | break; 163 | case TIMESTAMP, TIMESTAMPTZ: 164 | timestamp.toString(app); 165 | break; 166 | default: 167 | } 168 | } 169 | 170 | string toString() const { 171 | import std.array : appender; 172 | 173 | auto app = appender!string; 174 | toString(app); 175 | return app[]; 176 | } 177 | 178 | bool opEquals(PgSQLValue other) const { 179 | if (isString && other.isString) 180 | return peek!string == other.peek!string; 181 | if (isScalar == other.isScalar) { 182 | if (isFloat || other.isFloat) 183 | return get!double == other.get!double; 184 | return get!long == other.get!long; 185 | } 186 | if (isTime == other.isTime) 187 | return get!Duration == other.get!Duration; 188 | if (isTimestamp == other.isTimestamp) 189 | return get!SysTime == other.get!SysTime; 190 | return isNull == other.isNull; 191 | } 192 | 193 | T get(T)(lazy T def) const => !isNull ? get!T : def; 194 | 195 | // dfmt off 196 | T get(T)() @trusted const if (isScalarType!T && !is(T == enum)) { 197 | switch(type_) with (PgType) { 198 | case CHAR: return cast(T)*cast(char*)&p; 199 | case INT2: return cast(T)*cast(short*)&p; 200 | case INT4: return cast(T)*cast(int*)&p; 201 | case INT8: return cast(T)*cast(long*)&p; 202 | case REAL: return cast(T)*cast(float*)&p; 203 | case DOUBLE: return cast(T)*cast(double*)&p; 204 | default: 205 | } 206 | throw new PgSQLErrorException(text("Cannot convert ", type_.columnTypeName, 207 | " to " ~ T.stringof)); 208 | } 209 | // dfmt on 210 | 211 | T get(T : SysTime)() @trusted const { 212 | switch (type_) with (PgType) { 213 | case TIMESTAMP, TIMESTAMPTZ: 214 | return timestamp.toSysTime; 215 | default: 216 | } 217 | throw new PgSQLErrorException(text("Cannot convert ", type_.columnTypeName, 218 | " to " ~ T.stringof)); 219 | } 220 | 221 | T get(T : DateTime)() @trusted const { 222 | switch (type_) with (PgType) { 223 | case TIMESTAMP, TIMESTAMPTZ: 224 | return timestamp.toDateTime; 225 | default: 226 | } 227 | throw new PgSQLErrorException(text("Cannot convert ", type_.columnTypeName, 228 | " to " ~ T.stringof)); 229 | } 230 | 231 | T get(T : TimeOfDay)() @trusted const { 232 | switch (type_) with (PgType) { 233 | case TIME, TIMETZ: 234 | return time.toTimeOfDay; 235 | case TIMESTAMP, TIMESTAMPTZ: 236 | return timestamp.toTimeOfDay; 237 | default: 238 | } 239 | throw new PgSQLErrorException(text("Cannot convert ", type_.columnTypeName, 240 | " to " ~ T.stringof)); 241 | } 242 | 243 | T get(T : Duration)() @trusted const { 244 | switch (type_) with (PgType) { 245 | case TIME, TIMETZ, 246 | TIMESTAMP, TIMESTAMPTZ: 247 | return timestamp.time.toDuration; 248 | default: 249 | } 250 | throw new PgSQLErrorException(text("Cannot convert ", type_.columnTypeName, 251 | " to " ~ T.stringof)); 252 | } 253 | 254 | T get(T)() @trusted const if (is(T : Date)) { 255 | switch (type_) with (PgType) { 256 | case DATE, 257 | TIMESTAMP, TIMESTAMPTZ: 258 | return timestamp.date; 259 | default: 260 | } 261 | throw new PgSQLErrorException(text("Cannot convert ", type_.columnTypeName, 262 | " to " ~ T.stringof)); 263 | } 264 | 265 | T get(T)() const if (is(T == enum)) 266 | => cast(T)get!(OriginalType!T); 267 | 268 | T get(T)() const @trusted if (isArray!T && !is(T == enum)) { 269 | switch (type_) with (PgType) { 270 | case NUMERIC, 271 | MONEY, 272 | BIT, VARBIT, 273 | INET, CIDR, MACADDR, MACADDR8, 274 | UUID, JSON, XML, 275 | TEXT, NAME, 276 | VARCHAR, CHARA, 277 | BYTEA: 278 | static if (isStaticArray!T) 279 | return cast(T)arr[0 .. T.sizeof]; 280 | else 281 | return cast(T)arr.dup; 282 | default: 283 | } 284 | throw new PgSQLErrorException(text("Cannot convert ", type_.columnTypeName, " to array")); 285 | } 286 | 287 | T peek(T)(lazy T def) const => !isNull ? peek!T : def; 288 | 289 | T peek(T)() const if (is(T == struct) || isStaticArray!T) => get!T; 290 | 291 | T peek(T)() @trusted const if (is(T == U[], U)) { 292 | switch (type_) with (PgType) { 293 | case NUMERIC, 294 | MONEY, 295 | BIT, VARBIT, 296 | INET, CIDR, MACADDR, MACADDR8, 297 | UUID, JSON, XML, 298 | TEXT, NAME, 299 | VARCHAR, CHARA: 300 | return cast(T)arr; 301 | default: 302 | } 303 | throw new PgSQLErrorException(text("Cannot convert ", type_.columnTypeName, " to array")); 304 | } 305 | 306 | size_t toHash() const @nogc @trusted pure nothrow 307 | => *cast(size_t*)&p ^ (type_ << 24); 308 | 309 | @property nothrow @nogc { 310 | 311 | bool isNull() const => type_ == PgType.NULL; 312 | 313 | bool isUnknown() const => type_ == PgType.UNKNOWN; 314 | 315 | PgType type() const => type_; 316 | 317 | bool isString() const { 318 | switch (type_) with (PgType) { 319 | case NUMERIC, 320 | MONEY, 321 | BIT, VARBIT, 322 | INET, CIDR, MACADDR, MACADDR8, 323 | UUID, JSON, XML, 324 | TEXT, NAME, 325 | VARCHAR, CHARA: 326 | return true; 327 | default: 328 | } 329 | return false; 330 | } 331 | 332 | bool isScalar() const { 333 | switch (type_) with (PgType) { 334 | case BOOL, CHAR, 335 | INT2, INT4, INT8, 336 | REAL, DOUBLE: 337 | return true; 338 | default: 339 | } 340 | return false; 341 | } 342 | 343 | bool isFloat() const => type_ == PgType.REAL || type_ == PgType.DOUBLE; 344 | 345 | bool isTime() const => type_ == PgType.TIME || type_ == PgType.TIMETZ; 346 | 347 | bool isDate() const => type_ == PgType.DATE; 348 | 349 | bool isTimestamp() const => type_ == PgType.TIMESTAMP || type_ == PgType.TIMESTAMPTZ; 350 | } 351 | 352 | private: 353 | static if (size_t.sizeof > 4) { 354 | union { 355 | struct { 356 | uint length; 357 | PgType type_; 358 | ubyte p; 359 | } 360 | 361 | ubyte[] _arr; 362 | PgSQLTimestamp timestamp; 363 | } 364 | 365 | @property const(ubyte)[] arr() @trusted const { 366 | union Array { 367 | const ubyte[] arr; 368 | size_t length; 369 | } 370 | 371 | auto u = Array(_arr); 372 | u.length &= uint.max; 373 | return u.arr; 374 | } 375 | 376 | @property ubyte[] arr(ubyte[] arr) @trusted 377 | in (arr.length <= uint.max) { 378 | const type = type_; 379 | _arr = arr; 380 | type_ = type; 381 | return arr; 382 | } 383 | } else { 384 | PgType type_; 385 | union { 386 | ubyte p; 387 | ubyte[] arr; 388 | PgSQLTimestamp timestamp; 389 | } 390 | } 391 | } 392 | 393 | /// Field (column) description, part of RowDescription message. 394 | struct PgSQLColumn { 395 | /// Name of the field 396 | string name; 397 | 398 | /// If the field can be identified as a column of a specific table, 399 | /// the object ID of the table; otherwise zero. 400 | int table; 401 | 402 | /// If the field can be identified as a column of a specific table, 403 | /// the attribute number of the column; otherwise zero. 404 | short columnId; 405 | 406 | /// The data type size (see pg_type.typlen). 407 | /// Note that negative values denote variable-width types. 408 | short length; 409 | 410 | /// The object ID of the field's data type. 411 | PgType type; 412 | 413 | /// The type modifier (see pg_attribute.atttypmod). 414 | /// The meaning of the modifier is type-specific. 415 | int modifier; 416 | 417 | /// The format code being used for the field. Currently will be zero (text) 418 | /// or one (binary). In a RowDescription returned from the statement variant 419 | /// of Describe, the format code is not yet known and will always be zero. 420 | FormatCode format; 421 | } 422 | 423 | struct PgSQLHeader { 424 | PgSQLColumn[] cols; 425 | alias cols this; 426 | 427 | this(size_t count, ref InputPacket packet) @trusted { 428 | import std.array; 429 | 430 | cols = uninitializedArray!(PgSQLColumn[])(count); 431 | foreach (ref def; cols) { 432 | def.name = packet.eatz().idup; 433 | def.table = packet.eat!int; 434 | def.columnId = packet.eat!short; 435 | def.type = packet.eat!PgType; 436 | def.length = packet.eat!short; 437 | def.modifier = packet.eat!int; 438 | def.format = packet.eat!FormatCode; 439 | } 440 | } 441 | } 442 | 443 | struct PgSQLTime { 444 | union { 445 | uint _usec; 446 | struct { 447 | version (LittleEndian) { 448 | private byte[3] pad; 449 | byte moffset; 450 | } else { 451 | byte moffset; 452 | private byte[3] pad; 453 | } 454 | } 455 | } 456 | 457 | ubyte hour, minute, second; 458 | byte hoffset; 459 | 460 | this(ubyte h, ubyte m, ubyte s) { 461 | hour = h; 462 | minute = m; 463 | second = s; 464 | } 465 | 466 | private this(uint usec, ubyte h, ubyte m, ubyte s, byte hoffset = 0) pure { 467 | _usec = usec; 468 | hour = h; 469 | minute = m; 470 | second = s; 471 | this.hoffset = hoffset; 472 | } 473 | 474 | @property uint usec() const => _usec & 0xFFFFFF; 475 | 476 | @property uint usec(uint usec) 477 | in (usec <= 0xFFFFFF) { 478 | _usec = usec | moffset << 24; 479 | return usec; 480 | } 481 | 482 | invariant ((_usec & 0xFFFFFF) < 1_000_000); 483 | invariant (hour < 24 && minute < 60 && second < 60); 484 | invariant (0 <= hour + hoffset && hour + hoffset < 24); 485 | invariant (0 <= minute + moffset && minute + moffset < 60); 486 | 487 | Duration toDuration() const => usecs((hour + hoffset) * 3600_000_000L + ( 488 | minute + moffset) * 60_000_000L + 489 | second * 1_000_000L + 490 | usec); 491 | 492 | TimeOfDay toTimeOfDay() const => TimeOfDay(hour + hoffset, minute + moffset, second); 493 | 494 | void toString(W)(ref W w) const { 495 | w.formattedWrite("%02d:%02d:%02d", hour, minute, second); 496 | if (usec) { 497 | uint usecabv = usec; 498 | if (usecabv % 1000 == 0) 499 | usecabv /= 1000; 500 | if (usecabv % 100 == 0) 501 | usecabv /= 100; 502 | if (usecabv % 10 == 0) 503 | usecabv /= 10; 504 | w.formattedWrite(".%d", usecabv); 505 | } 506 | if (hoffset || moffset) { 507 | if (hoffset < 0 || moffset < 0) { 508 | w.formattedWrite("-%02d", -hoffset); 509 | if (moffset) 510 | w.formattedWrite(":%02d", -moffset); 511 | } else { 512 | w.formattedWrite("+%02d", hoffset); 513 | if (moffset) 514 | w.formattedWrite(":%02d", moffset); 515 | } 516 | } 517 | } 518 | } 519 | 520 | struct PgSQLTimestamp { 521 | Date date; 522 | align(size_t.sizeof) PgSQLTime time; 523 | 524 | SysTime toSysTime() const { 525 | auto datetime = DateTime(date.year, date.month, date.day, time.hour, time.minute, time 526 | .second); 527 | if (time.hoffset || time.moffset) { 528 | const offset = time.hoffset.hours + time.moffset.minutes; 529 | return SysTime(datetime, time.usec.usecs, new immutable SimpleTimeZone(offset)); 530 | } 531 | return SysTime(datetime, time.usec.usecs); 532 | } 533 | 534 | TimeOfDay toTimeOfDay() const => time.toTimeOfDay(); 535 | 536 | DateTime toDateTime() const => DateTime(date, time.toTimeOfDay()); 537 | 538 | void toString(R)(ref R app) const { 539 | app.formattedWrite("%04d-%02d-%02d ", date.year, date.month, date.day); 540 | time.toString(app); 541 | } 542 | } 543 | 544 | auto parseDate(ref scope const(char)[] x) { 545 | int year = x.parse!int(0); 546 | x.skip('-'); 547 | int month = x.parse!int(0); 548 | x.skip('-'); 549 | int day = x.parse!int(0); 550 | return Date(year, month, day); 551 | } 552 | 553 | auto parsePgSQLTime(ref scope const(char)[] x) { 554 | auto hour = x.parse!uint(0); 555 | x.skip(':'); 556 | auto minute = x.parse!uint(0); 557 | x.skip(':'); 558 | auto second = x.parse!uint(0); 559 | uint usecs; 560 | 561 | if (x.length && x[0] == '.') { 562 | x.skip(); 563 | const len = x.length; 564 | usecs = x.parse!uint(0); 565 | const d = 6 - (len - x.length); 566 | if (d < 0 || d > 5) 567 | throw new PgSQLProtocolException("Bad datetime string format"); 568 | 569 | usecs *= 10 ^^ d; 570 | } 571 | 572 | byte hoffset, moffset; 573 | 574 | if (x.length) { 575 | auto sign = x[0] == '-' ? -1 : 1; 576 | x.skip(); 577 | 578 | hoffset = cast(byte)(sign * x.parse!int(0)); 579 | if (x.length) { 580 | x.skip(':'); 581 | moffset = cast(byte)(sign * x.parse!int(0)); 582 | } 583 | } 584 | 585 | auto res = PgSQLTime(usecs, cast(ubyte)hour, cast(ubyte)minute, cast(ubyte)second, hoffset); 586 | res.moffset = moffset; 587 | return res; 588 | } 589 | 590 | auto parsePgSQLTimestamp(ref scope const(char)[] x) { 591 | auto date = parseDate(x); 592 | x.skip(); 593 | auto time = parsePgSQLTime(x); 594 | return PgSQLTimestamp(date, time); 595 | } 596 | 597 | private: 598 | void skip(ref scope const(char)[] x, char ch) { 599 | if (!x.length || x[0] != ch) 600 | throw new PgSQLProtocolException("Bad datetime string format"); 601 | x = x[1 .. $]; 602 | } 603 | 604 | void skip(ref scope const(char)[] x) { 605 | if (!x.length) 606 | throw new PgSQLProtocolException("Bad datetime string format"); 607 | x = x[1 .. $]; 608 | } 609 | -------------------------------------------------------------------------------- /database/querybuilder.d: -------------------------------------------------------------------------------- 1 | module database.querybuilder; 2 | 3 | import std.meta; 4 | import std.traits; 5 | 6 | import database.sqlbuilder; 7 | import database.util; 8 | 9 | enum Placeholder; 10 | 11 | @safe: 12 | 13 | alias del(T) = QueryBuilder!(SB.del!T); 14 | 15 | alias select(T...) = QueryBuilder!(SB.select!T); 16 | 17 | alias update(T, OR or = OR.None) = QueryBuilder!(SB.update!(T, or)); 18 | 19 | struct QueryBuilder(SB sb, Args...) { 20 | enum sql = sb.sql; 21 | alias args = Args; 22 | alias all = AS!(sql, args); 23 | 24 | template opDispatch(string key) { 25 | template opDispatch(A...) { 26 | static if (A.length && allSatisfy!(isType, A)) 27 | alias opDispatch = QueryBuilder!( 28 | mixin("sb.", key, "!A")(), 29 | Args); 30 | else { 31 | import std.algorithm : move; 32 | 33 | alias expr = AS!(); 34 | alias args = AS!(); 35 | static foreach (a; A) { 36 | static if (is(typeof(move(a)))) { 37 | args = AS!(args, a); 38 | expr = AS!(expr, Placeholder); 39 | } else 40 | expr = AS!(expr, a); 41 | } 42 | 43 | alias opDispatch = QueryBuilder!( 44 | __traits(getMember, sb, key)(putPlaceholder!expr(Args.length + 1)), 45 | Args, args); 46 | } 47 | } 48 | } 49 | 50 | alias all this; 51 | } 52 | 53 | unittest { 54 | @snakeCase 55 | struct User { 56 | @sqlkey() uint id; 57 | string name; 58 | } 59 | 60 | uint id = 1; 61 | auto name = "name"; 62 | 63 | alias s = Alias!(select!"name".from!User 64 | .where!("id=", id)); 65 | static assert(s.sql == `SELECT name FROM "user" WHERE id=$1`); 66 | assert(s.args == AliasSeq!(id)); 67 | 68 | alias u = Alias!(update!User.set!("name=", name) 69 | .from!User 70 | .where!("id=", id)); 71 | static assert(u.sql == `UPDATE "user" SET name=$1 FROM "user" WHERE id=$2`); 72 | assert(u.args == AliasSeq!(name, id)); 73 | 74 | alias d = Alias!(del!User.where!("id=", id)); 75 | static assert(d.sql == `DELETE FROM "user" WHERE id=$1`); 76 | assert(d.args == AliasSeq!(id)); 77 | } 78 | 79 | private: 80 | 81 | alias AS = AliasSeq; 82 | 83 | string putPlaceholder(T...)(uint start = 1) { 84 | import std.conv : text; 85 | 86 | auto s = ""; 87 | alias i = start; 88 | foreach (a; T) { 89 | static if (is(a == Placeholder)) 90 | s ~= text('$', i++); 91 | else 92 | s ~= text(a); 93 | } 94 | return s; 95 | } 96 | -------------------------------------------------------------------------------- /database/row.d: -------------------------------------------------------------------------------- 1 | module database.row; 2 | 3 | enum Strict { 4 | yes, 5 | yesIgnoreNull, 6 | no, 7 | } 8 | 9 | package(database) 10 | struct Row(Value, Header, E: 11 | Exception, alias hashOf, alias Mixin) { 12 | import std.algorithm; 13 | import std.traits; 14 | import std.conv : text; 15 | 16 | ref auto opDispatch(string key)() const => this[key]; 17 | 18 | ref auto opIndex(string key) const { 19 | if (auto index = find(hashOf(key), key)) 20 | return values[index - 1]; 21 | throw new E("Column '" ~ key ~ "' was not found in this result set"); 22 | } 23 | 24 | ref auto opIndex(size_t index) => values[index]; 25 | 26 | const(Value)* opBinaryRight(string op : "in")(string key) pure const { 27 | if (auto index = find(hashOf(key), key)) 28 | return &values[index - 1]; 29 | return null; 30 | } 31 | 32 | int opApply(int delegate(const ref Value value) del) const { 33 | foreach (ref v; values) 34 | if (auto ret = del(v)) 35 | return ret; 36 | return 0; 37 | } 38 | 39 | int opApply(int delegate(ref size_t, const ref Value) del) const { 40 | foreach (ref i, ref v; values) 41 | if (auto ret = del(i, v)) 42 | return ret; 43 | return 0; 44 | } 45 | 46 | int opApply(int delegate(const ref string, const ref Value) del) const { 47 | foreach (i, ref v; values) 48 | if (auto ret = del(_header[i].name, v)) 49 | return ret; 50 | return 0; 51 | } 52 | 53 | void toString(R)(ref R app) const { 54 | app.formattedWrite("%s", values); 55 | } 56 | 57 | string toString() @trusted const { 58 | import std.conv : to; 59 | 60 | return values.to!string; 61 | } 62 | 63 | string[] toStringArray(size_t start = 0, size_t end = size_t.max) const 64 | in (start <= end) { 65 | import std.array; 66 | 67 | if (end > values.length) 68 | end = values.length; 69 | if (start > values.length) 70 | start = values.length; 71 | 72 | string[] result = uninitializedArray!(string[])(end - start); 73 | foreach (i, ref s; result) 74 | s = values[i].toString(); 75 | return result; 76 | } 77 | 78 | void get(T, Strict strict = Strict.yesIgnoreNull)(ref T x) 79 | if (isAggregateType!T) { 80 | import std.typecons; 81 | 82 | static if (isTuple!T) { 83 | static if (strict != Strict.no) 84 | if (x.length >= values.length) 85 | throw new E(text("Column ", x.length, " is out of range for this result set")); 86 | foreach (i, ref f; x.tupleof) { 87 | static if (strict != Strict.yes) { 88 | if (!this[i].isNull) 89 | f = this[i].get!(Unqual!(typeof(f))); 90 | } else 91 | f = this[i].get!(Unqual!(typeof(f))); 92 | } 93 | } else 94 | structurize!strict(x); 95 | } 96 | 97 | T get(T)() if (!isAggregateType!T) => this[0].get!T; 98 | 99 | T get(T, Strict strict = Strict.yesIgnoreNull)() if (isAggregateType!T) { 100 | T result; 101 | get!(T, strict)(result); 102 | return result; 103 | } 104 | 105 | Value[] values; 106 | alias values this; 107 | 108 | @property Header header() => _header; 109 | 110 | package(database): 111 | @property void header(Header header) { 112 | _header = header; 113 | auto headerLen = header.length; 114 | auto idealLen = headerLen + (headerLen >> 2); 115 | auto indexLen = index_.length; 116 | 117 | index_[] = 0; 118 | 119 | if (indexLen < idealLen) { 120 | if (indexLen < 32) 121 | indexLen = 32; 122 | 123 | while (indexLen < idealLen) 124 | indexLen <<= 1; 125 | 126 | index_.length = indexLen; 127 | } 128 | 129 | auto mask = indexLen - 1; 130 | assert((indexLen & mask) == 0); 131 | 132 | values.length = headerLen; 133 | foreach (index, ref column; header) { 134 | auto hash = hashOf(column.name) & mask; 135 | uint probe; 136 | 137 | for (;;) { 138 | if (index_[hash] == 0) { 139 | index_[hash] = cast(uint)index + 1; 140 | break; 141 | } 142 | 143 | hash = (hash + ++probe) & mask; 144 | } 145 | } 146 | } 147 | 148 | auto ref get_(size_t index) => values[index]; // TODO 149 | 150 | mixin Mixin; 151 | 152 | private: 153 | Header _header; 154 | uint[] index_; 155 | } 156 | -------------------------------------------------------------------------------- /database/sqlbuilder.d: -------------------------------------------------------------------------------- 1 | module database.sqlbuilder; 2 | // dfmt off 3 | import 4 | database.util, 5 | std.ascii, 6 | std.meta, 7 | std.range, 8 | std.traits; 9 | // dfmt on 10 | import std.string; 11 | public import database.traits : SQLName; 12 | 13 | enum State { 14 | none = "", 15 | create = "CREATE TABLE ", 16 | createNX = "CREATE TABLE IF NOT EXISTS ", 17 | del = "DELETE FROM ", 18 | from = " FROM ", 19 | groupBy = " GROUP BY ", 20 | having = " HAVING ", 21 | insert = "INSERT ", 22 | limit = " LIMIT ", 23 | offset = " OFFSET ", 24 | orderBy = " ORDER BY ", 25 | returning = " RETURNING ", 26 | select = "SELECT ", 27 | set = " SET ", 28 | update = "UPDATE ", 29 | where = " WHERE " 30 | } 31 | 32 | enum OR { 33 | None = "", 34 | Abort = "OR ABORT ", 35 | Fail = "OR FAIL ", 36 | Ignore = "OR IGNORE ", 37 | Replace = "OR REPLACE ", 38 | Rollback = "OR ROLLBACK " 39 | } 40 | 41 | @safe: 42 | 43 | string placeholders(size_t x) pure nothrow { 44 | import std.conv : to; 45 | 46 | if (!x) 47 | return ""; 48 | 49 | auto s = "$1"; 50 | foreach (i; 2 .. x + 1) 51 | s ~= ",$" ~ i.to!string; 52 | return s; 53 | } 54 | 55 | /** An instance of a query building process */ 56 | struct SQLBuilder { 57 | string sql; 58 | alias sql this; 59 | State state; 60 | 61 | this(string sql, State STATE = State.none) { 62 | this.sql = STATE.startsWithWhite ? sql : STATE ~ sql; 63 | state = STATE; 64 | } 65 | 66 | static SB create(T)() if (isAggregateType!T) { 67 | enum sql = createTable!T; 68 | return sql; 69 | } 70 | 71 | /// 72 | unittest { 73 | assert(SQLBuilder.create!User == `CREATE TABLE IF NOT EXISTS "User"("name" TEXT,"age" INT)`); 74 | static assert(!__traits(compiles, SQLBuilder.create!int)); 75 | } 76 | 77 | alias insert(T) = insert!(OR.None, T); 78 | 79 | static SB insert(OR or = OR.None, T)() if (isAggregateType!T) 80 | => SB(make!(or ~ "INTO " ~ quote(SQLName!T) ~ '(', 81 | ")VALUES(" ~ placeholders(ColumnCount!T) ~ ')', T), State.insert); 82 | 83 | /// 84 | unittest { 85 | assert(SQLBuilder.insert!User == `INSERT INTO "User"("name","age")VALUES($1,$2)`); 86 | assert(SQLBuilder.insert!Message == `INSERT INTO "msg"("contents")VALUES($1)`); 87 | } 88 | 89 | /// 90 | static SB select(Fields...)() if (Fields.length) { 91 | static if (allSatisfy!(isString, Fields)) { 92 | enum sql = [Fields].join(','); 93 | return SB(sql, State.select); 94 | } else { 95 | enum sql = quoteJoin([staticMap!(SQLName, Fields)]); 96 | return SB(sql, State.select).from(NoDuplicates!(staticMap!(ParentName, Fields))); 97 | } 98 | } 99 | 100 | /// 101 | unittest { 102 | assert(SQLBuilder.select!("only_one") == "SELECT only_one"); 103 | assert(SQLBuilder.select!("hey", "you") == "SELECT hey,you"); 104 | assert(SQLBuilder.select!(User.name) == `SELECT "name" FROM "User"`); 105 | assert(SQLBuilder.select!(User.name, User.age) == `SELECT "name","age" FROM "User"`); 106 | } 107 | 108 | /// 109 | static SB selectAllFrom(Tables...)() if (allSatisfy!(isAggregateType, Tables)) { 110 | string[] fields, tables; 111 | foreach (S; Tables) { 112 | { 113 | enum tblName = SQLName!S; 114 | foreach (N; FieldNameTuple!S) 115 | fields ~= tblName.quote ~ '.' ~ ColumnName!(S, N).quote; 116 | 117 | tables ~= tblName; 118 | } 119 | } 120 | return SB("SELECT " ~ fields.join(',') ~ " FROM " 121 | ~ quoteJoin(tables), State.from); 122 | } 123 | /// 124 | unittest { 125 | assert(SQLBuilder.selectAllFrom!(Message, User) == 126 | `SELECT "msg"."rowid","msg"."contents","User"."name","User"."age" FROM "msg","User"`); 127 | } 128 | 129 | /// 130 | mixin(Clause!("from", "set", "select")); 131 | 132 | /// 133 | SB from(Tables...)(Tables tables) 134 | if (Tables.length > 1 && allSatisfy!(isString, Tables)) 135 | => from([tables].join(',')); 136 | 137 | /// 138 | SB from(Tables...)() if (Tables.length && allSatisfy!(isAggregateType, Tables)) 139 | => from(quoteJoin([staticMap!(SQLName, Tables)])); 140 | 141 | /// 142 | mixin(Clause!("set", "update")); 143 | 144 | /// 145 | static SB update(OR or = OR.None, S: 146 | const(char)[])(S table) 147 | => SB(or ~ table, State.update); 148 | 149 | /// 150 | static SB update(T, OR or = OR.None)() if (isAggregateType!T) 151 | => SB(or ~ quote(SQLName!T), State.update); 152 | 153 | /// 154 | static SB updateAll(T, OR or = OR.None)() if (isAggregateType!T) 155 | => SB(make!("UPDATE " ~ or ~ quote(SQLName!T) ~ " SET ", "=?", T), State.set); 156 | 157 | /// 158 | unittest { 159 | assert(SQLBuilder.update("User") == `UPDATE User`); 160 | assert(SQLBuilder.update!User == `UPDATE "User"`); 161 | assert(SQLBuilder.updateAll!User == `UPDATE "User" SET "name"=$1,"age"=$2`); 162 | } 163 | 164 | /// 165 | mixin(Clause!("where", "set", "from", "del")); 166 | 167 | /// 168 | static SB del(Table)() if (isAggregateType!Table) 169 | => del(quote(SQLName!Table)); 170 | 171 | /// 172 | static SB del(string table) 173 | => SB(table, State.del); 174 | 175 | /// 176 | unittest { 177 | assert(SQLBuilder.del!User.where("name=$1") == 178 | `DELETE FROM "User" WHERE name=$1`); 179 | assert(SQLBuilder.del!User.returning("*") == 180 | `DELETE FROM "User" RETURNING *`); 181 | } 182 | 183 | /// 184 | mixin(Clause!("using", "del")); 185 | 186 | /// 187 | mixin(Clause!("groupBy", "from", "where")); 188 | 189 | /// 190 | mixin(Clause!("having", "from", "where", "groupBy")); 191 | 192 | /// 193 | mixin(Clause!("orderBy", "from", "where", "groupBy", "having")); 194 | 195 | /// 196 | mixin(Clause!("limit", "from", "where", "groupBy", "having", "orderBy")); 197 | 198 | /// 199 | mixin(Clause!("offset", "limit")); 200 | 201 | /// 202 | mixin(Clause!("returning")); 203 | 204 | SB opCall(const(char)[] expr) { 205 | sql ~= expr; 206 | return this; 207 | } 208 | 209 | private: 210 | enum Clause(string name, prevStates...) = 211 | "SB " ~ name ~ "(const(char)[] expr)" ~ 212 | (prevStates.length ? "in(state == State." ~ [prevStates].join!(string[])( 213 | " || state == State.") ~ `, "Wrong SQL: ` ~ name ~ ` after " ~ state)` : "") 214 | ~ "{ sql ~= " ~ (__traits(hasMember, State, name) ? 215 | "(state = State." ~ name ~ ")" : `" ` ~ name.toUpper ~ ` "`) ~ " ~ expr; 216 | return this;}"; 217 | 218 | template make(string prefix, string suffix, T) if (isAggregateType!T) { 219 | mixin getSQLFields!(prefix, suffix, T); 220 | enum make = sql!sqlFields; 221 | } 222 | } 223 | 224 | /// 225 | unittest { 226 | // This will map to a "User" table in our database 227 | struct User { 228 | string name; 229 | int age; 230 | } 231 | 232 | assert(SB.create!User == `CREATE TABLE IF NOT EXISTS "User"("name" TEXT,"age" INT)`); 233 | 234 | auto q = SB.select!"name" 235 | .from!User 236 | .where("age=$1"); 237 | 238 | // The properties `sql` can be used to access the generated sql 239 | assert(q.sql == `SELECT name FROM "User" WHERE age=$1`); 240 | 241 | /// We can decorate structs and fields to give them different names in the database. 242 | @as("msg") struct Message { 243 | @as("rowid") int id; 244 | string contents; 245 | } 246 | 247 | // Note that virtual "rowid" field is handled differently -- it will not be created 248 | // by create(), and not inserted into by insert() 249 | 250 | assert(SB.create!Message == `CREATE TABLE IF NOT EXISTS "msg"("contents" TEXT)`); 251 | 252 | auto q2 = SB.insert!Message; 253 | assert(q2 == `INSERT INTO "msg"("contents") VALUES($1)`); 254 | } 255 | 256 | unittest { 257 | import std.algorithm.iteration : uniq; 258 | import std.algorithm.searching : count; 259 | 260 | alias C = ColumnName; 261 | 262 | // Make sure all these generate the same sql statement 263 | auto sql = [ 264 | SB.select!(`"msg"."rowid"`, `"msg"."contents"`).from(`"msg"`) 265 | .where(`"msg"."rowid"=$1`).sql, 266 | SB.select!(`"msg"."rowid"`, `"msg"."contents"`) 267 | .from!Message 268 | .where(C!(Message.id) ~ "=$1").sql, 269 | SB.select!(C!(Message.id), C!(Message.contents)) 270 | .from!Message 271 | .where(`"msg"."rowid"=$1`).sql, 272 | SB.selectAllFrom!Message.where(`"msg"."rowid"=$1`).sql 273 | ]; 274 | assert(count(uniq(sql)) == 1); 275 | } 276 | 277 | private: 278 | 279 | enum isString(alias x) = __traits(compiles, { const(char)[] s = x; }); 280 | 281 | bool startsWithWhite(S)(S s) 282 | => s.length && s[0].isWhite; 283 | 284 | SB createTable(T)() { 285 | import std.conv : to; 286 | 287 | string s; 288 | static foreach (A; __traits(getAttributes, T)) 289 | static if (is(typeof(A) : const(char)[])) 290 | static if (A.length) { 291 | static if (A.startsWithWhite) 292 | s ~= A; 293 | else 294 | s ~= ' ' ~ A; 295 | } 296 | alias FIELDS = Fields!T; 297 | string[] fields, keys, pkeys; 298 | 299 | static foreach (I, colName; ColumnNames!T) 300 | static if (colName.length) { 301 | { 302 | static if (colName != "rowid") { 303 | string field = quote(colName) ~ ' ', 304 | type = SQLTypeOf!(FIELDS[I]), 305 | constraints; 306 | } 307 | static foreach (A; __traits(getAttributes, T.tupleof[I])) 308 | static if (is(typeof(A) == sqlkey)) { 309 | static if (A.key.length) 310 | keys ~= "FOREIGN KEY(" ~ quote(colName) ~ ") REFERENCES " ~ A.key; 311 | else 312 | pkeys ~= colName; 313 | } else static if (colName != "rowid" && is(typeof(A) == sqltype)) 314 | type = A.type; 315 | else static if (is(typeof(A) : const(char)[])) 316 | static if (A.length) { 317 | static if (A.startsWithWhite) 318 | constraints ~= A; 319 | else 320 | constraints ~= ' ' ~ A; 321 | } 322 | static if (colName != "rowid") { 323 | field ~= type ~ constraints; 324 | enum member = T.init.tupleof[I]; 325 | if (member != FIELDS[I].init) 326 | field ~= " default " ~ quote(member.to!string, '\''); 327 | fields ~= field; 328 | } 329 | } 330 | } 331 | if (pkeys.length) 332 | keys ~= "PRIMARY KEY(" ~ quoteJoin(pkeys) ~ ')'; 333 | 334 | return SB(quote(SQLName!T) ~ '(' ~ join(fields ~ keys, ',') ~ ')' 335 | ~ s, State.createNX); 336 | } 337 | 338 | package(database) alias SB = SQLBuilder; 339 | -------------------------------------------------------------------------------- /database/sqlite/db.d: -------------------------------------------------------------------------------- 1 | module database.sqlite.db; 2 | 3 | // dfmt off 4 | import 5 | etc.c.sqlite3, 6 | database.sqlbuilder, 7 | database.sqlite, 8 | database.util; 9 | // dfmt on 10 | 11 | /// Setup code for tests 12 | version (unittest) template TEST(string dbname = "") { 13 | struct User { 14 | string name; 15 | int age; 16 | } 17 | 18 | struct Message { 19 | @as("rowid") int id; 20 | string content; 21 | int byUser; 22 | } 23 | 24 | mixin database.sqlite.TEST!(dbname, SQLite3DB); 25 | } 26 | 27 | // Returned from select-type methods where the row type is known 28 | struct QueryResult(T) { 29 | Query query; 30 | alias query this; 31 | 32 | void popFront() { 33 | step(); 34 | } 35 | 36 | @property T front() => this.get!T; 37 | } 38 | 39 | unittest { 40 | QueryResult!int q; 41 | assert(q.empty); 42 | } 43 | 44 | /// A Database with query building capabilities 45 | struct SQLite3DB { 46 | SQLite3 db; 47 | alias db this; 48 | bool autoCreateTable = true; 49 | 50 | this(string name, int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, int busyTimeout = 500) { 51 | db = SQLite3(name, flags, busyTimeout); 52 | } 53 | 54 | bool create(T)() { 55 | auto q = query(SB.create!T); 56 | q.step(); 57 | return q.lastCode == SQLITE_DONE; 58 | } 59 | 60 | auto selectAllWhere(T, string expr, A...)(auto ref A args) if (expr.length) 61 | => QueryResult!T(query(SB.selectAllFrom!T.where(expr), args)); 62 | 63 | T selectOneWhere(T, string expr, A...)(auto ref A args) if (expr.length) { 64 | auto q = query(SB.selectAllFrom!T.where(expr), args); 65 | if (q.step()) 66 | return q.get!T; 67 | throw new SQLEx("No match"); 68 | } 69 | 70 | T selectOneWhere(T, string expr, T defValue, A...)(auto ref A args) 71 | if (expr.length) { 72 | auto q = query(SB.selectAllFrom!T.where(expr), args); 73 | return q.step() ? q.get!T : defValue; 74 | } 75 | 76 | T selectRow(T)(ulong row) => selectOneWhere!(T, "rowid=?")(row); 77 | 78 | unittest { 79 | mixin TEST; 80 | import std.array : array; 81 | import std.algorithm.iteration : fold; 82 | 83 | db.create!User; 84 | db.insert(User("jonas", 55)); 85 | db.insert(User("oliver", 91)); 86 | db.insert(User("emma", 12)); 87 | db.insert(User("maria", 27)); 88 | 89 | auto users = db.selectAllWhere!(User, "age > ?")(20).array; 90 | auto total = fold!((a, b) => User("", a.age + b.age))(users); 91 | 92 | assert(total.age == 55 + 91 + 27); 93 | assert(db.selectOneWhere!(User, "age = ?")(27).name == "maria"); 94 | assert(db.selectRow!User(2).age == 91); 95 | } 96 | 97 | int insert(OR or = OR.None, T)(T row) { 98 | if (autoCreateTable && !hasTable(SQLName!T)) { 99 | if (!create!T) 100 | return 0; 101 | } 102 | db.insert!or(row).step(); 103 | return db.changes; 104 | } 105 | 106 | int replaceInto(T)(T s) => insert!(OR.Replace, T)(s); 107 | 108 | int delWhere(T, string expr, A...)(auto ref A args) if (expr.length) { 109 | query(SB.del!T.where(expr), args).step(); 110 | return db.changes; 111 | } 112 | 113 | unittest { 114 | mixin TEST; 115 | User user = {"jonas", 45}; 116 | assert(db.insert(user)); 117 | assert(db.query("select name from User where age = 45").step()); 118 | assert(!db.query("select age from User where name = 'xxx'").step()); 119 | assert(db.delWhere!(User, "age = ?")(45)); 120 | } 121 | } 122 | 123 | unittest { 124 | // Test quoting by using keyword as table and column name 125 | mixin TEST; 126 | struct Group { 127 | int group; 128 | } 129 | 130 | Group a = {3}; 131 | db.insert(a); 132 | Group b = db.selectOneWhere!(Group, `"group"=3`); 133 | assert(a == b); 134 | } 135 | 136 | unittest { 137 | import std.datetime; 138 | 139 | mixin TEST; 140 | 141 | struct S { 142 | int id; 143 | Date date; 144 | DateTime dt; 145 | Duration d; 146 | } 147 | 148 | S a = { 149 | 1, Date(2022, 2, 22), DateTime(2022, 2, 22, 22, 22, 22), dur!"msecs"(666) 150 | }; 151 | db.insert(a); 152 | S b = db.selectOneWhere!(S, `id=1`); 153 | assert(a == b); 154 | } 155 | -------------------------------------------------------------------------------- /database/sqlite/package.d: -------------------------------------------------------------------------------- 1 | module database.sqlite; 2 | 3 | import std.conv : to; 4 | 5 | // dfmt off 6 | import 7 | std.datetime, 8 | std.exception, 9 | std.meta, 10 | std.string, 11 | std.traits, 12 | std.typecons, 13 | etc.c.sqlite3, 14 | database.sqlbuilder, 15 | database.util; 16 | // dfmt on 17 | 18 | version (Windows) { 19 | // manually link in dub.sdl 20 | } else version (linux) { 21 | pragma(lib, "sqlite3"); 22 | } else version (OSX) { 23 | pragma(lib, "sqlite3"); 24 | } else version (Posix) { 25 | pragma(lib, "libsqlite3"); 26 | } else { 27 | pragma(msg, "You need to manually link in the SQLite library."); 28 | } 29 | 30 | class SQLiteException : DBException { 31 | this(string msg, string file = __FILE__, size_t line = __LINE__) pure @safe { 32 | super(msg, file, line); 33 | } 34 | } 35 | 36 | /// Setup code for tests 37 | version (unittest) package template TEST(string dbname = "", T = SQLite3) { 38 | T db = { 39 | static if (dbname.length) { 40 | tryRemove(dbname ~ ".db"); 41 | return T(dbname ~ ".db"); 42 | } else 43 | return T(":memory:"); 44 | }(); 45 | } 46 | 47 | package { 48 | alias SQLEx = SQLiteException; 49 | alias toz = toStringz; 50 | 51 | void checkError(string prefix)(sqlite3* db, int rc) { 52 | if (rc < 0) 53 | rc = sqlite3_errcode(db); 54 | enforce!SQLEx(rc == SQLITE_OK || rc == SQLITE_ROW || rc == SQLITE_DONE, 55 | prefix ~ " (" ~ rc.to!string ~ "): " ~ db.errmsg); 56 | } 57 | } 58 | 59 | private template Manager(alias ptr, alias freeptr) { 60 | mixin("alias ", __traits(identifier, ptr), " this;"); 61 | 62 | ~this() { 63 | free(); 64 | } 65 | 66 | void free() { 67 | freeptr(ptr); 68 | ptr = null; 69 | } 70 | } 71 | 72 | struct ExpandedSql { 73 | char* ptr; 74 | mixin Manager!(ptr, sqlite3_free); 75 | } 76 | 77 | alias RCExSql = RefCounted!(ExpandedSql, RefCountedAutoInitialize.no); 78 | 79 | @property { 80 | auto errmsg(sqlite3* db) => sqlite3_errmsg(db).toStr; 81 | 82 | int changes(sqlite3* db) 83 | in (db) => sqlite3_changes(db); 84 | /// Return the 'rowid' produced by the last insert statement 85 | long lastRowid(sqlite3* db) 86 | in (db) => sqlite3_last_insert_rowid(db); 87 | 88 | void lastRowid(sqlite3* db, long rowid) 89 | in (db) => sqlite3_set_last_insert_rowid(db, rowid); 90 | 91 | int totalChanges(sqlite3* db) 92 | in (db) => sqlite3_total_changes(db); 93 | 94 | string sql(sqlite3_stmt* stmt) 95 | in (stmt) => sqlite3_sql(stmt).toStr; 96 | 97 | RCExSql expandedSql(sqlite3_stmt* stmt) 98 | in (stmt) => RCExSql(ExpandedSql(sqlite3_expanded_sql(stmt))); 99 | } 100 | 101 | enum EpochDateTime = DateTime(2000, 1, 1, 0, 0, 0); 102 | 103 | private enum canConvertToInt(T) = __traits(isIntegral, T) || 104 | is(T : Date) || is(T : DateTime) || is(T : Duration); 105 | 106 | /// Represents a sqlite3 statement 107 | alias Statement = Query; 108 | 109 | struct Query { 110 | int lastCode; 111 | int argIndex; 112 | sqlite3_stmt* stmt; 113 | alias stmt this; 114 | 115 | /// Construct a query from the string 'sql' into database 'db' 116 | this(A...)(sqlite3* db, in char[] sql, auto ref A args) 117 | in (db) 118 | in (sql.length) { 119 | lastCode = -1; 120 | _count = 1; 121 | int rc = sqlite3_prepare_v2(db, sql.toz, -1, &stmt, null); 122 | db.checkError!"Prepare failed: "(rc); 123 | this.db = db; 124 | set(args); 125 | } 126 | 127 | this(this) { 128 | _count++; 129 | } 130 | 131 | ~this() { 132 | if(--_count == 0) 133 | close(); 134 | } 135 | 136 | /// Close the statement 137 | void close() { 138 | sqlite3_finalize(stmt); 139 | stmt = null; 140 | } 141 | 142 | /// Bind these args in order to '?' marks in statement 143 | pragma(inline, true) void set(A...)(auto ref A args) { 144 | foreach (a; args) 145 | db.checkError!"Bind failed: "(bindArg(++argIndex, a)); 146 | } 147 | 148 | int clear() 149 | in (stmt) => sqlite3_clear_bindings(stmt); 150 | 151 | // Find column by name 152 | int findColumn(string name) 153 | in (stmt) { 154 | import core.stdc.string : strcmp; 155 | 156 | auto ptr = name.toz; 157 | int count = sqlite3_column_count(stmt); 158 | for (int i = 0; i < count; i++) { 159 | if (strcmp(sqlite3_column_name(stmt, i), ptr) == 0) 160 | return i; 161 | } 162 | return -1; 163 | } 164 | 165 | auto ref front() => this; 166 | 167 | alias popFront = step; 168 | 169 | /// Get current row (and column) as a basic type 170 | T get(T, int COL = 0)() if (!isAggregateType!T) 171 | in (stmt) { 172 | if (lastCode == -1) 173 | step(); 174 | return getArg!T(COL); 175 | } 176 | 177 | /// Map current row to the fields of the given T 178 | T get(T, int _ = 0)() if (isAggregateType!T) 179 | in (stmt) { 180 | if (lastCode == -1) 181 | step(); 182 | T t; 183 | int i = void; 184 | foreach (N; FieldNameTuple!T) { 185 | i = findColumn(ColumnName!(T, N)); 186 | if (i >= 0) 187 | __traits(getMember, t, N) = getArg!(typeof(__traits(getMember, t, N)))(i); 188 | } 189 | return t; 190 | } 191 | 192 | /// Get current row as a tuple 193 | Tuple!T get(T...)() { 194 | Tuple!T t; 195 | foreach (I, Ti; T) 196 | t[I] = get!(Ti, I)(); 197 | return t; 198 | } 199 | 200 | /// Step the SQL statement; move to next row of the result set. Return `false` if there are no more rows 201 | bool step() 202 | in (stmt) { 203 | db.checkError!"Step failed"(lastCode = sqlite3_step(stmt)); 204 | return lastCode == SQLITE_ROW; 205 | } 206 | 207 | @property bool empty() { 208 | if (lastCode == -1) 209 | step(); 210 | return lastCode != SQLITE_ROW; 211 | } 212 | 213 | /// Reset the statement, to step through the resulting rows again. 214 | int reset() 215 | in (stmt) => sqlite3_reset(stmt); 216 | 217 | private: 218 | sqlite3* db; 219 | size_t _count; 220 | 221 | int bindArg(int pos, const char[] arg) { 222 | static if (size_t.sizeof > 4) 223 | return sqlite3_bind_text64(stmt, pos, arg.ptr, arg.length, null, SQLITE_UTF8); 224 | else 225 | return sqlite3_bind_text(stmt, pos, arg.ptr, cast(int)arg.length, null); 226 | } 227 | 228 | int bindArg(int pos, double arg) 229 | => sqlite3_bind_double(stmt, pos, arg); 230 | 231 | int bindArg(T)(int pos, T x) if (canConvertToInt!T) { 232 | static if (is(T : Date)) 233 | return sqlite3_bind_int(stmt, pos, x.dayOfGregorianCal); 234 | else static if (is(T : DateTime)) 235 | return sqlite3_bind_int64(stmt, pos, (x - EpochDateTime).total!"usecs"); 236 | else static if (is(T : Duration)) 237 | return sqlite3_bind_int64(stmt, pos, x.total!"usecs"); 238 | else static if (T.sizeof > 4) 239 | return sqlite3_bind_int64(stmt, pos, x); 240 | else 241 | return sqlite3_bind_int(stmt, pos, x); 242 | } 243 | 244 | int bindArg(int pos, void[] arg) { 245 | static if (size_t.sizeof > 4) 246 | return sqlite3_bind_blob64(stmt, pos, arg.ptr, arg.length, null); 247 | else 248 | return sqlite3_bind_blob(stmt, pos, arg.ptr, cast(int)arg.length, null); 249 | } 250 | 251 | int bindArg(int pos, typeof(null)) 252 | => sqlite3_bind_null(stmt, pos); 253 | 254 | T getArg(T)(int pos) { 255 | const typ = sqlite3_column_type(stmt, pos); 256 | static if (canConvertToInt!T) { 257 | enforce!SQLEx(typ == SQLITE_INTEGER, "Column is not an integer"); 258 | static if (is(T : Date)) 259 | return Date(sqlite3_column_int(stmt, pos)); 260 | else static if (is(T : DateTime)) 261 | return EpochDateTime + dur!"usecs"(sqlite3_column_int64(stmt, pos)); 262 | else static if (is(T : Duration)) 263 | return dur!"usecs"(sqlite3_column_int64(stmt, pos)); 264 | else static if (T.sizeof > 4) 265 | return sqlite3_column_int64(stmt, pos); 266 | else 267 | return cast(T)sqlite3_column_int(stmt, pos); 268 | } else static if (isSomeString!T) { 269 | if (typ == SQLITE_NULL) 270 | return T.init; 271 | int size = sqlite3_column_bytes(stmt, pos); 272 | return cast(T)sqlite3_column_text(stmt, pos)[0 .. size].dup; 273 | } else static if (isFloatingPoint!T) { 274 | enforce!SQLEx(typ != SQLITE_BLOB, "Column cannot convert to a real"); 275 | return sqlite3_column_double(stmt, pos); 276 | } else { 277 | if (typ == SQLITE_NULL) 278 | return T.init; 279 | enforce!SQLEx(typ == SQLITE3_TEXT || typ == SQLITE_BLOB, 280 | "Column is not a blob or string"); 281 | auto ptr = sqlite3_column_blob(stmt, pos); 282 | int size = sqlite3_column_bytes(stmt, pos); 283 | static if (isStaticArray!T) { 284 | enforce!SQLEx(size == T.sizeof, "Column size does not match array size"); 285 | return cast(T)ptr[0 .. T.sizeof]; 286 | } else 287 | return cast(T)ptr[0 .. size].dup; 288 | } 289 | } 290 | } 291 | 292 | /// 293 | unittest { 294 | mixin TEST; 295 | 296 | auto q = db.query("create table TEST(a INT, b INT)"); 297 | assert(!q.step()); 298 | 299 | q = db.query("insert into TEST values(?, ?)"); 300 | q.set(1, 2); 301 | assert(!q.step()); 302 | q = db.query("select b from TEST where a == ?", 1); 303 | assert(q.step()); 304 | assert(q.get!int == 2); 305 | assert(!q.step()); 306 | 307 | q = db.query("select a,b from TEST where b == ?", 2); 308 | // Try not stepping... assert(q.step()); 309 | assert(q.get!(int, int) == tuple(1, 2)); 310 | 311 | struct Test { 312 | int a, b; 313 | } 314 | 315 | auto test = q.get!Test; 316 | assert(test.a == 1 && test.b == 2); 317 | 318 | assert(!q.step()); 319 | 320 | q.reset(); 321 | assert(q.step()); 322 | assert(q.get!(int, int) == tuple(1, 2)); 323 | 324 | // Test exception 325 | assertThrown!SQLEx(q.get!(byte[])); 326 | } 327 | 328 | /// A sqlite3 database 329 | struct SQLite3 { 330 | 331 | /++ Create a SQLite3 from a database file. If file does not exist, the 332 | database will be initialized as new 333 | +/ 334 | this(string dbFile, int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, int busyTimeout = 500) { 335 | int rc = sqlite3_open_v2(dbFile.toz, &db, flags, null); 336 | if (!rc) 337 | sqlite3_busy_timeout(db, busyTimeout); 338 | if (rc != SQLITE_OK) { 339 | auto errmsg = db.errmsg; 340 | sqlite3_close(db); 341 | db = null; 342 | throw new SQLEx("Could not open database: " ~ errmsg); 343 | } 344 | } 345 | 346 | /// Execute multiple statements 347 | int execSQL(string sql, out string errmsg) { 348 | char* err_msg = void; 349 | int rc = sqlite3_exec(db, sql.toz, null, null, &err_msg); 350 | errmsg = err_msg.toStr; 351 | return rc; 352 | } 353 | 354 | /// Execute an sql statement directly, binding the args to it 355 | bool exec(A...)(string sql, auto ref A args) { 356 | auto q = query(sql, args); 357 | q.step(); 358 | return q.lastCode == SQLITE_DONE || q.lastCode == SQLITE_ROW; 359 | } 360 | 361 | /// 362 | unittest { 363 | mixin TEST; 364 | assert(db.exec("CREATE TABLE Test(name STRING)")); 365 | assert(db.exec("INSERT INTO Test VALUES(?)", "hey")); 366 | } 367 | 368 | /// Return 'true' if database contains the given table 369 | bool hasTable(string table) => query( 370 | "SELECT name FROM sqlite_master WHERE type='table' AND name=?", 371 | table).step(); 372 | 373 | /// 374 | unittest { 375 | mixin TEST; 376 | assert(!db.hasTable("MyTable")); 377 | db.exec("CREATE TABLE MyTable(id INT)"); 378 | assert(db.hasTable("MyTable")); 379 | } 380 | 381 | /// 382 | unittest { 383 | mixin TEST; 384 | assert(db.exec("CREATE TABLE MyTable(name STRING)")); 385 | assert(db.exec("INSERT INTO MyTable VALUES(?)", "hey")); 386 | assert(db.lastRowid == 1); 387 | assert(db.exec("INSERT INTO MyTable VALUES(?)", "ho")); 388 | assert(db.lastRowid == 2); 389 | // Only insert updates the last rowid 390 | assert(db.exec("UPDATE MyTable SET name=? WHERE rowid=?", "woo", 1)); 391 | assert(db.lastRowid == 2); 392 | db.lastRowid = 9; 393 | assert(db.lastRowid == 9); 394 | } 395 | 396 | /// Create query from string and args to bind 397 | auto query(A...)(in char[] sql, auto ref A args) 398 | => Query(db, sql, args); 399 | 400 | private auto make(State state, string prefix, string suffix, T)(T s) 401 | if (isAggregateType!T) { 402 | mixin getSQLFields!(prefix, suffix, T); 403 | // Skips "rowid" field 404 | static if (I >= 0) 405 | return Query(db, SB(sql!sqlFields, state), 406 | s.tupleof[0 .. I], s.tupleof[I + 1 .. $]); 407 | else 408 | return Query(db, SB(sql!sqlFields, state), s.tupleof); 409 | } 410 | 411 | auto insert(OR or = OR.None, T)(T s) if (isAggregateType!T) { 412 | import std.array : replicate; 413 | 414 | enum qms = ",?".replicate(ColumnCount!T); 415 | return make!(State.insert, or ~ "INTO " ~ 416 | quote(SQLName!T) ~ '(', ") VALUES(" ~ 417 | (qms.length ? qms[1 .. $] : qms) ~ ')')(s); 418 | } 419 | 420 | bool begin() => exec("begin"); 421 | 422 | bool commit() => exec("commit"); 423 | 424 | bool rollback() => exec("rollback"); 425 | 426 | unittest { 427 | mixin TEST; 428 | assert(db.begin()); 429 | assert(db.exec("CREATE TABLE MyTable(name TEXT)")); 430 | assert(db.exec("INSERT INTO MyTable VALUES(?)", "hey")); 431 | assert(db.rollback()); 432 | assert(!db.hasTable("MyTable")); 433 | assert(db.begin()); 434 | assert(db.exec("CREATE TABLE MyTable(name TEXT)")); 435 | assert(db.exec("INSERT INTO MyTable VALUES(?)", "hey")); 436 | assert(db.commit()); 437 | assert(db.hasTable("MyTable")); 438 | } 439 | 440 | auto insertID() => lastRowid(db); 441 | 442 | sqlite3* db; 443 | alias db this; 444 | 445 | void close() { 446 | sqlite3_close_v2(db); 447 | db = null; 448 | } 449 | } 450 | -------------------------------------------------------------------------------- /database/traits.d: -------------------------------------------------------------------------------- 1 | module database.traits; 2 | 3 | import database.util; 4 | import std.datetime; 5 | import std.meta; 6 | import std.traits; 7 | 8 | version (unittest) package(database) { 9 | struct User { 10 | string name; 11 | int age; 12 | } 13 | 14 | @as("msg") struct Message { 15 | @as("rowid") int id; 16 | string contents; 17 | } 18 | } 19 | 20 | /// Provide a custom name in the database for a field or table 21 | struct as { // @suppress(dscanner.style.phobos_naming_convention) 22 | string name; 23 | } 24 | 25 | /// Ignore a field, it is not considered part of the database data. 26 | enum ignore; // @suppress(dscanner.style.phobos_naming_convention) 27 | 28 | enum optional; // @suppress(dscanner.style.phobos_naming_convention) 29 | 30 | enum { 31 | default0 = "default '0'", 32 | 33 | notnull = "not null", 34 | 35 | /// Mark a specific column as unique on the table 36 | unique = "unique" 37 | } 38 | 39 | /// Mark a field as the primary key or foreign key of the table 40 | struct sqlkey { // @suppress(dscanner.style.phobos_naming_convention) 41 | string key; 42 | } 43 | 44 | struct sqltype { // @suppress(dscanner.style.phobos_naming_convention) 45 | string type; 46 | } 47 | 48 | /// foreign key 49 | enum foreign(alias field) = sqlkey(ColumnName!(field, true)); 50 | 51 | /// Get the keyname of `T`, return empty if fails 52 | template KeyName(alias T, string defaultName = T.stringof) { 53 | static if (hasUDA!(T, ignore)) 54 | enum KeyName = ""; 55 | else static if (hasUDA!(T, as)) 56 | enum KeyName = getUDAs!(T, as)[0].name; 57 | else 58 | static foreach (attr; __traits(getAttributes, T)) 59 | static if (is(typeof(KeyName) == void) && is(typeof(attr("")))) 60 | enum KeyName = attr(defaultName); 61 | static if (is(typeof(KeyName) == void)) 62 | enum KeyName = defaultName; 63 | } 64 | 65 | /// Get the sqlname of `T` 66 | alias SQLName = KeyName; 67 | 68 | /// 69 | unittest { 70 | static assert(SQLName!User == "User"); 71 | static assert(SQLName!Message == "msg"); 72 | } 73 | 74 | /// Generate a column name given a field in T. 75 | template ColumnName(T, string field) if (isAggregateType!T) { 76 | enum ColumnName = SQLName!(__traits(getMember, T, field), field); 77 | } 78 | 79 | /// Return the qualifed column name of the given struct field 80 | enum ColumnName(alias field, bool brackets = false) = 81 | ParentName!field ~ (brackets ? '(' ~ quote(SQLName!field) ~ ')' : '.' ~ quote(SQLName!field)); 82 | 83 | /// 84 | unittest { 85 | @as("msg") struct Message { 86 | @as("txt") string contents; 87 | } 88 | 89 | static assert(ColumnName!(User, "age") == "age"); 90 | static assert(ColumnName!(Message.contents) == `"msg"."txt"`); 91 | static assert(ColumnName!(User.age) == `"User"."age"`); 92 | static assert(ColumnName!(User.age, true) == `"User"("age")`); 93 | } 94 | 95 | template ColumnNames(T) { 96 | enum colName(string NAME) = ColumnName!(T, NAME); 97 | enum ColumnNames = staticMap!(colName, FieldNameTuple!T); 98 | } 99 | 100 | /// get column count except "rowid" field 101 | template ColumnCount(T) { 102 | enum colNames = ColumnNames!T, 103 | indexOfRowid = staticIndexOf!("rowid", colNames); 104 | static if (~indexOfRowid) 105 | enum ColumnCount = colNames.length - 1; 106 | else 107 | enum ColumnCount = colNames.length; 108 | } 109 | 110 | template SQLTypeOf(T) { 111 | static if (isSomeString!T) 112 | enum SQLTypeOf = "TEXT"; 113 | else static if (isFloatingPoint!T) { 114 | static if (T.sizeof == 4) 115 | enum SQLTypeOf = "REAL"; 116 | else 117 | enum SQLTypeOf = "DOUBLE PRECISION"; 118 | } else static if (isIntegral!T) { 119 | static if (T.sizeof <= 2) 120 | enum SQLTypeOf = "SMALLINT"; 121 | else static if (T.sizeof == 4) 122 | enum SQLTypeOf = "INT"; 123 | else 124 | enum SQLTypeOf = "BIGINT"; 125 | } else static if (isBoolean!T) 126 | enum SQLTypeOf = "BOOLEAN"; 127 | else static if (!isSomeString!T && !isScalarType!T) { 128 | version (USE_PGSQL) { 129 | static if (is(T : Date)) 130 | enum SQLTypeOf = "date"; 131 | else static if (is(T : DateTime)) 132 | enum SQLTypeOf = "timestamp"; 133 | else static if (is(T : SysTime)) 134 | enum SQLTypeOf = "timestamp with time zone"; 135 | else static if (is(T : TimeOfDay)) 136 | enum SQLTypeOf = "time"; 137 | else static if (is(T : Duration)) 138 | enum SQLTypeOf = "interval"; 139 | else 140 | enum SQLTypeOf = "bytea"; 141 | } else static if (is(T : Date)) 142 | enum SQLTypeOf = "INT"; 143 | else static if (is(T : DateTime) || is(T : Duration)) 144 | enum SQLTypeOf = "BIGINT"; 145 | else 146 | enum SQLTypeOf = "BLOB"; 147 | } else 148 | static assert(0, "Unsupported SQLType '" ~ T.stringof ~ '.'); 149 | } 150 | 151 | /// 152 | unittest { 153 | static assert(SQLTypeOf!int == "INT"); 154 | static assert(SQLTypeOf!string == "TEXT"); 155 | static assert(SQLTypeOf!float == "REAL"); 156 | static assert(SQLTypeOf!double == "DOUBLE PRECISION"); 157 | static assert(SQLTypeOf!bool == "BOOLEAN"); 158 | version (USE_PGSQL) { 159 | static assert(SQLTypeOf!Date == "date"); 160 | static assert(SQLTypeOf!DateTime == "timestamp"); 161 | static assert(SQLTypeOf!SysTime == "timestamp with time zone"); 162 | static assert(SQLTypeOf!TimeOfDay == "time"); 163 | static assert(SQLTypeOf!Duration == "interval"); 164 | static assert(SQLTypeOf!(ubyte[]) == "bytea"); 165 | } else { 166 | static assert(SQLTypeOf!Date == "INT"); 167 | static assert(SQLTypeOf!DateTime == "BIGINT"); 168 | static assert(SQLTypeOf!Duration == "BIGINT"); 169 | static assert(SQLTypeOf!(ubyte[]) == "BLOB"); 170 | } 171 | } 172 | 173 | enum isVisible(alias M) = __traits(getVisibility, M).length == 6; //public or export 174 | 175 | template isWritableDataMember(alias M) { 176 | alias TM = typeof(M); 177 | static if (is(AliasSeq!M) || hasUDA!(M, ignore)) 178 | enum isWritableDataMember = false; 179 | else static if (is(TM == enum)) 180 | enum isWritableDataMember = true; 181 | else static if (!fitsInString!TM || isSomeFunction!TM) 182 | enum isWritableDataMember = false; 183 | else static if (!is(typeof(() { M = TM.init; }()))) 184 | enum isWritableDataMember = false; 185 | else 186 | enum isWritableDataMember = isVisible!M; 187 | } 188 | 189 | template isReadableDataMember(alias M) { 190 | alias TM = typeof(M); 191 | static if (is(AliasSeq!M) || hasUDA!(M, ignore)) 192 | enum isReadableDataMember = false; 193 | else static if (is(TM == enum)) 194 | enum isReadableDataMember = true; 195 | else static if (!fitsInString!TM) 196 | enum isReadableDataMember = false; 197 | else static if (isSomeFunction!TM /* && return type is valueType*/ ) 198 | enum isReadableDataMember = true; 199 | else static if (!is(typeof({ TM x = M; }))) 200 | enum isReadableDataMember = false; 201 | else 202 | enum isReadableDataMember = isVisible!M; 203 | } 204 | 205 | /// Sort tables based on dependencies 206 | template sortTable(T...) if (T.length <= uint.max) { 207 | import std.meta; 208 | 209 | enum N = cast(uint)T.length; 210 | 211 | alias sortTable = AliasSeq!(); 212 | static foreach (i; sort()) 213 | sortTable = AliasSeq!(sortTable, T[i]); 214 | 215 | auto sort() 216 | out (result; result.length == N) { 217 | import std.string; 218 | 219 | uint[string] nameToIndex; 220 | foreach (i, Table; T) 221 | nameToIndex[quote(SQLName!Table)] = i; 222 | uint[][N] g; 223 | uint[N] in_; 224 | foreach (i, Table; T) { 225 | foreach (j, _; Table.tupleof) 226 | foreach (S; __traits(getAttributes, Table.tupleof[j])) 227 | static if (is(typeof(S) == sqlkey) && S.key.length) { 228 | g[nameToIndex[S.key[0 .. S.key.indexOf('(')]]] ~= i; 229 | in_[i]++; 230 | } 231 | } 232 | uint[] q, result; 233 | foreach (i; 0 .. N) 234 | if (!in_[i]) 235 | q ~= i; 236 | while (q.length) { 237 | uint u = q[0]; 238 | q = q[1 .. $]; 239 | result ~= u; 240 | foreach (v; g[u]) 241 | if (--in_[v] == 0) 242 | q ~= v; 243 | } 244 | return result; 245 | } 246 | } 247 | 248 | /// Returns whether table B depends on table A 249 | template dependsOn(A, B) { 250 | import std.string : startsWith; 251 | 252 | enum prefix = quote(SQLName!A) ~ '('; 253 | static foreach (j, _; B.tupleof) 254 | static foreach (S; __traits(getAttributes, B.tupleof[j])) 255 | static if (!is(typeof(dependsOn) == bool) && is(typeof(S) == sqlkey)) 256 | static if (S.key.startsWith(prefix)) { 257 | enum dependsOn = true; 258 | } 259 | static if (!is(typeof(dependsOn) == bool)) 260 | enum dependsOn = false; 261 | } 262 | 263 | private: 264 | 265 | enum fitsInString(T) = 266 | !isAssociativeArray!T && (!isArray!T || is(typeof(T.init[0]) == ubyte) || 267 | is(T == string)); 268 | 269 | package(database): 270 | 271 | enum ParentName(alias field) = quote(SQLName!(__traits(parent, field))); 272 | 273 | alias CutOut(size_t I, T...) = AliasSeq!(T[0 .. I], T[I + 1 .. $]); 274 | 275 | string putPlaceholders(string[] s) { 276 | import std.conv : to; 277 | 278 | string res; 279 | for (size_t i; i < s.length;) { 280 | version (NO_SQLQUOTE) 281 | res ~= s[i]; 282 | else { 283 | res ~= '"'; 284 | res ~= s[i]; 285 | res ~= '"'; 286 | } 287 | ++i; 288 | res ~= "=$" ~ i.to!string; 289 | if (i < s.length) 290 | res ~= ','; 291 | } 292 | return res[]; 293 | } 294 | 295 | template getSQLFields(string prefix, string suffix, T) { 296 | import std.meta; 297 | 298 | enum colNames = ColumnNames!T, 299 | I = staticIndexOf!("rowid", colNames), 300 | sql(S...) = prefix ~ (suffix == "=?" ? 301 | putPlaceholders([S]) : quoteJoin([S]) ~ suffix); 302 | // Skips "rowid" field 303 | static if (I >= 0) 304 | enum sqlFields = CutOut!(I, colNames); 305 | else 306 | enum sqlFields = colNames; 307 | } 308 | -------------------------------------------------------------------------------- /database/util.d: -------------------------------------------------------------------------------- 1 | module database.util; 2 | 3 | // dfmt off 4 | import core.time, 5 | database.sqlbuilder, 6 | std.exception, 7 | std.meta, 8 | std.socket, 9 | std.string, 10 | std.traits, 11 | std.typecons; 12 | // dfmt on 13 | 14 | public import database.traits; 15 | 16 | class DBException : Exception { 17 | this(string msg, string file = __FILE__, size_t line = __LINE__) pure @safe { 18 | super(msg, file, line); 19 | } 20 | } 21 | 22 | private: 23 | 24 | enum CharClass { 25 | Other, 26 | LowerCase, 27 | UpperCase, 28 | Underscore, 29 | Digit 30 | } 31 | 32 | CharClass classify(char ch) pure { 33 | import std.ascii; 34 | 35 | with (CharClass) { 36 | if (isLower(ch)) 37 | return LowerCase; 38 | if (isUpper(ch)) 39 | return UpperCase; 40 | if (isDigit(ch)) 41 | return Digit; 42 | if (ch == '_') 43 | return Underscore; 44 | return Other; 45 | } 46 | } 47 | 48 | public: 49 | S snakeCase(S)(S input, char sep = '_') { 50 | if (!input.length) 51 | return ""; 52 | char[128] buffer = void; 53 | size_t length; 54 | 55 | auto pcls = classify(input[0]); 56 | foreach (ch; input) { 57 | auto cls = classify(ch); 58 | switch (cls) with (CharClass) { 59 | case UpperCase: 60 | if (pcls != UpperCase && pcls != Underscore) 61 | buffer[length++] = sep; 62 | buffer[length++] = ch | ' '; 63 | break; 64 | case Digit: 65 | if (pcls != Digit) 66 | buffer[length++] = sep; 67 | goto default; 68 | default: 69 | buffer[length++] = ch; 70 | break; 71 | } 72 | pcls = cls; 73 | 74 | if (length >= buffer.length - 1) 75 | break; 76 | } 77 | return cast(S)buffer[0 .. length].dup; 78 | } 79 | 80 | unittest { 81 | static void test(string str, string expected) { 82 | auto result = str.snakeCase; 83 | assert(result == expected, str ~ ": " ~ result); 84 | } 85 | 86 | test("AA", "aa"); 87 | test("AaA", "aa_a"); 88 | test("AaA1", "aa_a_1"); 89 | test("AaA11", "aa_a_11"); 90 | test("_AaA1", "_aa_a_1"); 91 | test("_AaA11_", "_aa_a_11_"); 92 | test("aaA", "aa_a"); 93 | test("aaAA", "aa_aa"); 94 | test("aaAA1", "aa_aa_1"); 95 | test("aaAA11", "aa_aa_11"); 96 | test("authorName", "author_name"); 97 | test("authorBio", "author_bio"); 98 | test("authorPortraitId", "author_portrait_id"); 99 | test("authorPortraitID", "author_portrait_id"); 100 | test("coverURL", "cover_url"); 101 | test("coverImageURL", "cover_image_url"); 102 | } 103 | 104 | S camelCase(S, bool upper = false)(in S input, char sep = '_') { 105 | S output; 106 | bool upcaseNext = upper; 107 | foreach (c; input) { 108 | if (c != sep) { 109 | if (upcaseNext) { 110 | output ~= c.toUpper; 111 | upcaseNext = false; 112 | } else 113 | output ~= c.toLower; 114 | } else 115 | upcaseNext = true; 116 | } 117 | return output; 118 | } 119 | 120 | S pascalCase(S)(in S input, char sep = '_') 121 | => camelCase!(S, true)(input, sep); 122 | 123 | unittest { 124 | assert("c".camelCase == "c"); 125 | assert("c".pascalCase == "C"); 126 | assert("c_a".camelCase == "cA"); 127 | assert("ca".pascalCase == "Ca"); 128 | assert("camel".pascalCase == "Camel"); 129 | assert("Camel".camelCase == "camel"); 130 | assert("camel_case".pascalCase == "CamelCase"); 131 | assert("camel_camel_case".pascalCase == "CamelCamelCase"); 132 | assert("caMel_caMel_caSe".pascalCase == "CamelCamelCase"); 133 | assert("camel2_camel2_case".pascalCase == "Camel2Camel2Case"); 134 | assert("get_http_response_code".camelCase == "getHttpResponseCode"); 135 | assert("get2_http_response_code".camelCase == "get2HttpResponseCode"); 136 | assert("http_response_code".pascalCase == "HttpResponseCode"); 137 | assert("http_response_code_xyz".pascalCase == "HttpResponseCodeXyz"); 138 | } 139 | 140 | S quote(S)(S s, char q = '"') if (isSomeString!S) { 141 | version (NO_SQLQUOTE) 142 | return s; 143 | else 144 | return q ~ s ~ q; 145 | } 146 | 147 | S quoteJoin(S, bool leaveTail = false)(S[] s, char sep = ',', char q = '"') 148 | if (isSomeString!S) { 149 | import std.array; 150 | 151 | auto res = appender!S; 152 | for (size_t i; i < s.length; i++) { 153 | version (NO_SQLQUOTE) 154 | res ~= s[i]; 155 | else { 156 | res ~= q; 157 | res ~= s[i]; 158 | res ~= q; 159 | } 160 | if (leaveTail || i + 1 < s.length) 161 | res ~= sep; 162 | } 163 | return res[]; 164 | } 165 | 166 | T parse(T)(inout(char)[] data) if (isIntegral!T) 167 | => parse!T(data, 0); 168 | 169 | T parse(T)(ref inout(char)[] data, size_t startIndex = 0) if (isIntegral!T) 170 | in (startIndex <= data.length) { 171 | T x; 172 | auto i = startIndex; 173 | for (; i < data.length; ++i) { 174 | const c = data[i]; 175 | if (c < '0' || c > '9') 176 | break; 177 | x = x * 10 + (c ^ '0'); 178 | } 179 | data = data[i .. $]; 180 | return x; 181 | } 182 | 183 | package(database): 184 | 185 | auto toStr(T)(T ptr) => fromStringz(ptr).idup; 186 | 187 | template InputPacketMethods(E : Exception) { 188 | void expect(T)(T x) { 189 | if (x != eat!T) 190 | throw new E("Bad packet format"); 191 | } 192 | 193 | void skip(size_t count) 194 | in (count <= buf.length) { 195 | buf = buf[count .. $]; 196 | } 197 | 198 | auto countUntil(ubyte x, bool expect) { 199 | auto index = buf.countUntil(x); 200 | if (expect && (index < 0 || buf[index] != x)) 201 | throw new E("Bad packet format"); 202 | return index; 203 | } 204 | // dfmt off 205 | void skipLenEnc() { 206 | auto header = eat!ubyte; 207 | if (header >= 0xfb) { 208 | switch(header) { 209 | case 0xfb: return; 210 | case 0xfc: return skip(2); 211 | case 0xfd: return skip(3); 212 | case 0xfe: return skip(8); 213 | default: 214 | } 215 | throw new E("Bad packet format"); 216 | } 217 | } 218 | 219 | ulong eatLenEnc() { 220 | auto header = eat!ubyte; 221 | if (header < 0xfb) 222 | return header; 223 | 224 | switch(header) { 225 | case 0xfb: return 0; 226 | case 0xfc: return eat!ushort; 227 | case 0xfd: 228 | _l l = {lo_8: eat!ubyte, 229 | hi_16: eat!ushort}; 230 | return l.n; 231 | case 0xfe: 232 | _l l = {lo: eat!uint, 233 | hi: eat!uint}; 234 | return l.n; 235 | default: 236 | } 237 | throw new E("Bad packet format"); 238 | } 239 | // dfmt on 240 | 241 | auto remaining() const => buf.length; 242 | 243 | bool empty() const => buf.length == 0; 244 | } 245 | 246 | template OutputPacketMethods() { 247 | void putLenEnc(ulong x) { 248 | if (x < 0xfb) { 249 | put(cast(ubyte)x); 250 | } else if (x <= ushort.max) { 251 | put!ubyte(0xfc); 252 | put(cast(ushort)x); 253 | } else if (x <= 0xffffff) { 254 | put!ubyte(0xfd); 255 | _l l = {n: x}; 256 | put(l.lo_8); 257 | put(l.hi_16); 258 | } else { 259 | put!ubyte(0xfe); 260 | _l l = {n: x}; 261 | put(l.lo); 262 | put(l.hi); 263 | } 264 | } 265 | 266 | size_t length() const => pos; 267 | 268 | bool empty() const => pos == 0; 269 | } 270 | 271 | align(1) union _l { 272 | struct { 273 | version (LittleEndian) { 274 | uint lo; 275 | uint hi; 276 | } else { 277 | uint hi; 278 | uint lo; 279 | } 280 | } 281 | 282 | struct { 283 | version (LittleEndian) { 284 | ubyte lo_8; 285 | ushort hi_16; 286 | } else { 287 | byte[5] pad; 288 | ushort hi_16; 289 | ubyte lo_8; 290 | } 291 | } 292 | 293 | ulong n; 294 | } 295 | 296 | class DBSocket(E : Exception) : TcpSocket { 297 | import core.stdc.errno; 298 | 299 | @safe: 300 | this(in char[] host, ushort port) { 301 | super(new InternetAddress(host, port)); 302 | setOption(SocketOptionLevel.SOCKET, SocketOption.KEEPALIVE, true); 303 | setOption(SocketOptionLevel.TCP, SocketOption.TCP_NODELAY, true); 304 | setOption(SocketOptionLevel.SOCKET, SocketOption.SNDTIMEO, 30.seconds); 305 | setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, 30.seconds); 306 | } 307 | 308 | override void close() scope { 309 | shutdown(SocketShutdown.BOTH); 310 | super.close(); 311 | } 312 | 313 | void read(void[] buffer) { 314 | long len = void; 315 | 316 | for (size_t i; i < buffer.length; i += len) { 317 | len = receive(buffer[i .. $]); 318 | 319 | if (len > 0) 320 | continue; 321 | 322 | if (len == 0) 323 | throw new E("Server closed the connection"); 324 | 325 | if (errno == EINTR || errno == EAGAIN /* || errno == EWOULDBLOCK*/ ) 326 | len = 0; 327 | else 328 | throw new E("Received std.socket.Socket.ERROR: " ~ formatSocketError(errno)); 329 | } 330 | } 331 | 332 | void write(in void[] buffer) { 333 | long len = void; 334 | 335 | for (size_t i; i < buffer.length; i += len) { 336 | len = send(buffer[i .. $]); 337 | 338 | if (len > 0) 339 | continue; 340 | 341 | if (len == 0) 342 | throw new E("Server closed the connection"); 343 | 344 | if (errno == EINTR || errno == EAGAIN /* || errno == EWOULDBLOCK*/ ) 345 | len = 0; 346 | else 347 | throw new E("Sent std.socket.Socket.ERROR: " ~ formatSocketError(errno)); 348 | } 349 | } 350 | } 351 | -------------------------------------------------------------------------------- /dub.sdl: -------------------------------------------------------------------------------- 1 | name "database" 2 | description "Lightweight native MySQL/MariaDB & PostgreSQL driver" 3 | authors "Marcio Martins" "Shove" 4 | copyright "Copyright © 2017-2020, Marcio Martins, Shove" 5 | license "MIT" 6 | dependency ":mysql" path="." 7 | dependency ":postgresql" path="." 8 | sourcePaths "source" 9 | importPaths "source" 10 | subPackage { 11 | name "mysql" 12 | description "Lightweight native MySQL/MariaDB driver" 13 | dependency "database:util" path="." 14 | sourcePaths "database/mysql" 15 | importPaths "." 16 | } 17 | subPackage { 18 | name "postgresql" 19 | description "Lightweight native PostgreSQL driver" 20 | dependency "database:util" path="." 21 | sourcePaths "database/postgresql" 22 | configuration "default" { 23 | importPaths "." 24 | } 25 | configuration "noMD5Auth" { 26 | importPaths "." 27 | versions "NoMD5Auth" 28 | } 29 | } 30 | subPackage { 31 | name "sqlite" 32 | description "Lightweight SQLite3 driver" 33 | dependency "database:util" path="." 34 | libs "sqlite3" 35 | sourcePaths "database/sqlite" 36 | importPaths "." 37 | } 38 | subPackage { 39 | name "util" 40 | dflags "-preview=in" "-dip1008" 41 | sourceFiles "database/pool.d" "database/row.d" "database/querybuilder.d" "database/sqlbuilder.d" "database/traits.d" "database/util.d" 42 | excludedSourceFiles "source/*.d" 43 | importPaths "." 44 | } -------------------------------------------------------------------------------- /source/mysql.d: -------------------------------------------------------------------------------- 1 | module mysql; 2 | 3 | public import database.mysql; 4 | -------------------------------------------------------------------------------- /source/postgresql.d: -------------------------------------------------------------------------------- 1 | module postgresql; 2 | 3 | public import database.postgresql; 4 | --------------------------------------------------------------------------------