From ba941bfc839ac2d28f7327dd9f6dc32d8a3cf984 Mon Sep 17 00:00:00 2001 From: Steven Schveighoffer Date: Mon, 28 Oct 2019 18:41:57 -0400 Subject: [PATCH 01/14] Some safe tags added, needs more work. --- source/mysql/connection.d | 4 +-- source/mysql/exceptions.d | 24 +++++++++----- source/mysql/pool.d | 22 ++++++------ source/mysql/prepared.d | 46 ++++++++++++++++++++++---- source/mysql/protocol/comms.d | 13 ++++++-- source/mysql/protocol/packet_helpers.d | 2 ++ source/mysql/protocol/packets.d | 2 ++ source/mysql/protocol/sockets.d | 7 ++-- 8 files changed, 89 insertions(+), 31 deletions(-) diff --git a/source/mysql/connection.d b/source/mysql/connection.d index a8ab2d91..bc5c57aa 100644 --- a/source/mysql/connection.d +++ b/source/mysql/connection.d @@ -353,6 +353,7 @@ the connection when done). //TODO: All low-level commms should be moved into the mysql.protocol package. class Connection { + @safe: /+ The Connection is responsible for handshaking with the server to establish authentication. It then passes client preferences to the server, and @@ -565,7 +566,6 @@ package: } public: - /++ Construct opened connection. @@ -868,7 +868,7 @@ public: (TODO: The connection string needs work to allow for semicolons in its parts!) +/ //TODO: Replace the return value with a proper struct. - static string[] parseConnectionString(string cs) + static string[] parseConnectionString(string cs) @safe { string[] rv; rv.length = 5; diff --git a/source/mysql/exceptions.d b/source/mysql/exceptions.d index 8717a7f2..92b6bf95 100644 --- a/source/mysql/exceptions.d +++ b/source/mysql/exceptions.d @@ -9,7 +9,8 @@ An exception type to distinguish exceptions thrown by this package. +/ class MYX: Exception { - this(string msg, string file = __FILE__, size_t line = __LINE__) pure +@safe pure: + this(string msg, string file = __FILE__, size_t line = __LINE__) { super(msg, file, line); } @@ -26,12 +27,14 @@ class MYXReceived: MYX ushort errorCode; char[5] sqlState; - this(OKErrorPacket okp, string file, size_t line) pure +@safe pure: + + this(OKErrorPacket okp, string file, size_t line) { this(okp.message, okp.serverStatus, okp.sqlState, file, line); } - this(string msg, ushort errorCode, char[5] sqlState, string file, size_t line) pure + this(string msg, ushort errorCode, char[5] sqlState, string file, size_t line) { this.errorCode = errorCode; this.sqlState = sqlState; @@ -66,7 +69,8 @@ is no longer used. deprecated("No longer thrown by mysql-native. You can safely remove all handling of this exception from your code.") class MYXNotPrepared: MYX { - this(string file = __FILE__, size_t line = __LINE__) pure +@safe pure: + this(string file = __FILE__, size_t line = __LINE__) { super("The prepared statement has already been released.", file, line); } @@ -90,7 +94,8 @@ results in an exception derived from this. +/ class MYXWrongFunction: MYX { - this(string msg, string file = __FILE__, size_t line = __LINE__) pure +@safe pure: + this(string msg, string file = __FILE__, size_t line = __LINE__) { super(msg, file, line); } @@ -105,7 +110,8 @@ that return result sets (such as SELECT), even if the result set has zero elemen +/ class MYXResultRecieved: MYXWrongFunction { - this(string file = __FILE__, size_t line = __LINE__) pure +@safe pure: + this(string file = __FILE__, size_t line = __LINE__) { super( "A result set was returned. Use the query functions, not exec, "~ @@ -124,7 +130,8 @@ for commands that don't produce result sets (such as INSERT). +/ class MYXNoResultRecieved: MYXWrongFunction { - this(string msg, string file = __FILE__, size_t line = __LINE__) pure +@safe pure: + this(string msg, string file = __FILE__, size_t line = __LINE__) { super( "The executed query did not produce a result set. Use the exec "~ @@ -142,7 +149,8 @@ has been issued on the same connection. +/ class MYXInvalidatedRange: MYX { - this(string msg, string file = __FILE__, size_t line = __LINE__) pure +@safe pure: + this(string msg, string file = __FILE__, size_t line = __LINE__) { super(msg, file, line); } diff --git a/source/mysql/pool.d b/source/mysql/pool.d index 8772a048..a96b9d39 100644 --- a/source/mysql/pool.d +++ b/source/mysql/pool.d @@ -99,7 +99,8 @@ version(IncludeMySQLPool) string m_database; ushort m_port; SvrCapFlags m_capFlags; - void delegate(Connection) m_onNewConnection; + alias NewConnectionDelegate = void delegate(Connection) @safe; + NewConnectionDelegate m_onNewConnection; ConnectionPool!Connection m_pool; PreparedRegistrations!PreparedInfo preparedRegistrations; @@ -109,6 +110,7 @@ version(IncludeMySQLPool) } } + @safe: /// Sets up a connection pool with the provided connection settings. /// @@ -117,7 +119,7 @@ version(IncludeMySQLPool) this(string host, string user, string password, string database, ushort port = 3306, uint maxConcurrent = (uint).max, SvrCapFlags capFlags = defaultClientFlags, - void delegate(Connection) onNewConnection = null) + NewConnectionDelegate onNewConnection = null) { m_host = host; m_user = user; @@ -131,34 +133,34 @@ version(IncludeMySQLPool) ///ditto this(string host, string user, string password, string database, - ushort port, SvrCapFlags capFlags, void delegate(Connection) onNewConnection = null) + ushort port, SvrCapFlags capFlags, NewConnectionDelegate onNewConnection = null) { this(host, user, password, database, port, (uint).max, capFlags, onNewConnection); } ///ditto this(string host, string user, string password, string database, - ushort port, void delegate(Connection) onNewConnection) + ushort port, NewConnectionDelegate onNewConnection) { this(host, user, password, database, port, (uint).max, defaultClientFlags, onNewConnection); } ///ditto this(string connStr, uint maxConcurrent = (uint).max, SvrCapFlags capFlags = defaultClientFlags, - void delegate(Connection) onNewConnection = null) + NewConnectionDelegate onNewConnection = null) { auto parts = Connection.parseConnectionString(connStr); this(parts[0], parts[1], parts[2], parts[3], to!ushort(parts[4]), capFlags, onNewConnection); } ///ditto - this(string connStr, SvrCapFlags capFlags, void delegate(Connection) onNewConnection = null) + this(string connStr, SvrCapFlags capFlags, NewConnectionDelegate onNewConnection = null) { this(connStr, (uint).max, capFlags, onNewConnection); } ///ditto - this(string connStr, void delegate(Connection) onNewConnection) + this(string connStr, NewConnectionDelegate onNewConnection) { this(connStr, (uint).max, defaultClientFlags, onNewConnection); } @@ -209,7 +211,7 @@ version(IncludeMySQLPool) } } - private Connection createConnection() + private Connection createConnection() @safe { auto conn = new Connection(m_host, m_user, m_password, m_database, m_port, m_capFlags); @@ -221,13 +223,13 @@ version(IncludeMySQLPool) /// Get/set a callback delegate to be run every time a new connection /// is created. - @property void onNewConnection(void delegate(Connection) onNewConnection) + @property void onNewConnection(NewConnectionDelegate onNewConnection) { m_onNewConnection = onNewConnection; } ///ditto - @property void delegate(Connection) onNewConnection() + @property void delegate(Connection) @safe onNewConnection() { return m_onNewConnection; } diff --git a/source/mysql/prepared.d b/source/mysql/prepared.d index 385eaf7e..00497398 100644 --- a/source/mysql/prepared.d +++ b/source/mysql/prepared.d @@ -133,6 +133,20 @@ unittest } } +// temporary trait fix to allow setting/getting of @safe types from args. +private template isSafeType(T) +{ + U foo(U)(U t) { + static if(is(typeof(t = t))) + t = t; + return t; + } + + enum isSafeType = is(typeof(() @safe => foo(T.init))); +} +static assert(isSafeType!(int)); +static assert(isSafeType!(typeof(null))); + /++ Encapsulation of a prepared statement. @@ -159,7 +173,7 @@ package: ColumnSpecialization[] _columnSpecials; ulong _lastInsertID; - ExecQueryImplInfo getExecQueryImplInfo(uint statementId) + ExecQueryImplInfo getExecQueryImplInfo(uint statementId) @safe { return ExecQueryImplInfo(true, null, statementId, _headers, _inParams, _psa); } @@ -182,7 +196,7 @@ public: including parameter descriptions, and result set field descriptions, followed by an EOF packet. +/ - this(const(char[]) sql, PreparedStmtHeaders headers, ushort numParams) + this(const(char[]) sql, PreparedStmtHeaders headers, ushort numParams) @safe { this._sql = sql; this._headers = headers; @@ -191,6 +205,19 @@ public: _psa.length = numParams; } + private void _assignArgImpl(T)(size_t index, ref T val) + { + static if(isSafeType!T) + { + // override Variant's unsafe mechanism, we know everything is safe. + (() @trusted { _inParams[index] = val; })(); + } + else + { + _inParams[index] = val; + } + } + /++ Prepared statement parameter setter. @@ -225,7 +252,7 @@ public: enforce!MYX(index < _numParams, "Parameter index out of range."); - _inParams[index] = val; + _assignArgImpl(index, val); psn.pIndex = index; _psa[index] = psn; } @@ -338,6 +365,7 @@ public: void setArgs(Variant[] args, ParameterSpecialization[] psnList=null) { enforce!MYX(args.length == _numParams, "Param count supplied does not match prepared statement"); + // can't be @safe because of the postblit for Variant _inParams[] = args[]; if (psnList !is null) { @@ -369,13 +397,13 @@ public: Params: index = The zero based index +/ - void setNullArg(size_t index) + void setNullArg(size_t index) @safe { setArg(index, null); } /// Gets the SQL command for this prepared statement. - const(char)[] sql() + const(char)[] sql() pure @safe const { return _sql; } @@ -510,6 +538,7 @@ to factor out common functionality needed by both `Connection` and `MySQLPool`. package struct PreparedRegistrations(Payload) if( isPreparedRegistrationsPayload!Payload) { + @safe: /++ Lookup payload by sql string. @@ -561,8 +590,11 @@ package struct PreparedRegistrations(Payload) queueForRelease(sql); } + // Note: AA.clear does not invalidate any keys or values. In fact, it + // should really be safe/trusted, but is not. Therefore, we mark this + // as trusted. /// Eliminate all records of both registered AND queued-for-release statements. - void clear() + void clear() @trusted { static if(__traits(compiles, (){ int[int] aa; aa.clear(); })) directLookup.clear(); @@ -571,7 +603,7 @@ package struct PreparedRegistrations(Payload) } /// If already registered, simply returns the cached Payload. - Payload registerIfNeeded(const(char[]) sql, Payload delegate(const(char[])) doRegister) + Payload registerIfNeeded(const(char[]) sql, Payload delegate(const(char[])) @safe doRegister) out(info) { // I'm confident this can't currently happen, but diff --git a/source/mysql/protocol/comms.d b/source/mysql/protocol/comms.d index 94dfc9df..c107e25b 100644 --- a/source/mysql/protocol/comms.d +++ b/source/mysql/protocol/comms.d @@ -38,9 +38,18 @@ import mysql.protocol.packet_helpers; import mysql.protocol.packets; import mysql.protocol.sockets; + +// helper for getting around calling opEquals on two typeids +private bool tideq(const TypeInfo ti1, const TypeInfo ti2) @trusted +{ + return ti1 == ti2; +} +@safe: + /// Low-level comms code relating to prepared statements. package struct ProtocolPrepared { + @safe: import std.conv; import std.datetime; import std.variant; @@ -53,7 +62,7 @@ package struct ProtocolPrepared bma.length = bml; foreach (i; 0..inParams.length) { - if(inParams[i].type != typeid(typeof(null))) + if(!inParams[i].type.tideq(typeid(typeof(null)))) continue; size_t bn = i/8; size_t bb = i%8; @@ -108,7 +117,7 @@ package struct ProtocolPrepared enum SIGNED = 0; if (psa[i].chunkSize) longData= true; - if (inParams[i].type == typeid(typeof(null))) + if (inParams[i].type.tideq(typeid(typeof(null)))) { types[ct++] = SQLType.NULL; types[ct++] = SIGNED; diff --git a/source/mysql/protocol/packet_helpers.d b/source/mysql/protocol/packet_helpers.d index 12243b17..aacd313e 100644 --- a/source/mysql/protocol/packet_helpers.d +++ b/source/mysql/protocol/packet_helpers.d @@ -15,6 +15,8 @@ import mysql.protocol.extra_types; import mysql.protocol.sockets; import mysql.types; +@safe: + /++ Function to extract a time difference from a binary encoded row. diff --git a/source/mysql/protocol/packets.d b/source/mysql/protocol/packets.d index 8c449d97..920ca41f 100644 --- a/source/mysql/protocol/packets.d +++ b/source/mysql/protocol/packets.d @@ -13,6 +13,8 @@ import mysql.protocol.extra_types; import mysql.protocol.sockets; public import mysql.protocol.packet_helpers; +@safe: + void enforcePacketOK(string file = __FILE__, size_t line = __LINE__)(OKErrorPacket okp) { enforce(!okp.error, new MYXReceived(okp, file, line)); diff --git a/source/mysql/protocol/sockets.d b/source/mysql/protocol/sockets.d index cb065168..dbfc28d1 100644 --- a/source/mysql/protocol/sockets.d +++ b/source/mysql/protocol/sockets.d @@ -27,8 +27,8 @@ else alias PlainVibeDSocket = Object; ///ditto } -alias OpenSocketCallbackPhobos = PlainPhobosSocket function(string,ushort); -alias OpenSocketCallbackVibeD = PlainVibeDSocket function(string,ushort); +alias OpenSocketCallbackPhobos = PlainPhobosSocket function(string,ushort) @safe; +alias OpenSocketCallbackVibeD = PlainVibeDSocket function(string,ushort) @safe; enum MySQLSocketType { phobos, vibed } @@ -36,6 +36,7 @@ enum MySQLSocketType { phobos, vibed } /// Used to wrap both Phobos and Vibe.d sockets with a common interface. interface MySQLSocket { +@safe: void close(); @property bool connected() const; void read(ubyte[] dst); @@ -50,6 +51,7 @@ interface MySQLSocket /// Wraps a Phobos socket with the common interface class MySQLSocketPhobos : MySQLSocket { +@safe: private PlainPhobosSocket socket; /// The socket should already be open @@ -106,6 +108,7 @@ version(Have_vibe_core) { /// Wraps a Vibe.d socket with the common interface class MySQLSocketVibeD : MySQLSocket { + @safe: private PlainVibeDSocket socket; /// The socket should already be open From 99034d775eb607ebad58b4d46f5b5d712f57676c Mon Sep 17 00:00:00 2001 From: Steven Schveighoffer Date: Thu, 31 Oct 2019 11:35:36 -0400 Subject: [PATCH 02/14] Start at trying to replace Variant with a tagged union. --- dub.sdl | 3 +- source/mysql/exceptions.d | 7 +- source/mysql/prepared.d | 40 ++-- source/mysql/protocol/comms.d | 280 +++++++++---------------- source/mysql/protocol/packet_helpers.d | 6 +- source/mysql/types.d | 45 ++++ 6 files changed, 172 insertions(+), 209 deletions(-) diff --git a/dub.sdl b/dub.sdl index 2ea56cc1..8d698b25 100644 --- a/dub.sdl +++ b/dub.sdl @@ -5,6 +5,7 @@ copyright "Copyright (c) 2011-2019 Steve Teale, James W. Oliphant, Simen Endsj authors "Steve Teale" "James W. Oliphant" "Simen Endsjø" "Sönke Ludwig" "Sergey Shamov" "Nick Sabalausky" dependency "vibe-core" version="~>1.0" optional=true +dependency "taggedalgebraic" version="~>0.11.6" sourcePaths "source/" importPaths "source/" @@ -52,7 +53,7 @@ configuration "unittest-vibe-ut" { buildOptions "unittests" dependency "vibe-core" version="~>1.0" optional=false - + dependency "unit-threaded" version="~>0.7.45" debugVersions "MYSQLN_TESTS" diff --git a/source/mysql/exceptions.d b/source/mysql/exceptions.d index 92b6bf95..560e1b69 100644 --- a/source/mysql/exceptions.d +++ b/source/mysql/exceptions.d @@ -1,4 +1,4 @@ -/// Exceptions defined by mysql-native. +/// Exceptions defined by mysql-native. module mysql.exceptions; import std.algorithm; @@ -34,7 +34,7 @@ class MYXReceived: MYX this(okp.message, okp.serverStatus, okp.sqlState, file, line); } - this(string msg, ushort errorCode, char[5] sqlState, string file, size_t line) + this(string msg, ushort errorCode, char[5] sqlState, string file, size_t line) { this.errorCode = errorCode; this.sqlState = sqlState; @@ -50,7 +50,8 @@ if you receive this.) +/ class MYXProtocol: MYX { - this(string msg, string file, size_t line) pure +@safe pure: + this(string msg, string file, size_t line) { super(msg, file, line); } diff --git a/source/mysql/prepared.d b/source/mysql/prepared.d index 00497398..f4225b49 100644 --- a/source/mysql/prepared.d +++ b/source/mysql/prepared.d @@ -1,4 +1,4 @@ -/// Use a DB via SQL prepared statements. +/// Use a DB via SQL prepared statements. module mysql.prepared; import std.exception; @@ -29,11 +29,11 @@ then the chunk will be assumed to be the last one. struct ParameterSpecialization { import mysql.protocol.constants; - + size_t pIndex; //parameter number 0 - number of params-1 SQLType type = SQLType.INFER_FROM_D_TYPE; uint chunkSize; /// In bytes - uint delegate(ubyte[]) chunkDelegate; + uint delegate(ubyte[]) @safe chunkDelegate; } ///ditto alias PSN = ParameterSpecialization; @@ -177,11 +177,11 @@ package: { return ExecQueryImplInfo(true, null, statementId, _headers, _inParams, _psa); } - + public: /++ Constructor. You probably want `mysql.connection.prepare` instead of this. - + Call `mysqln.connection.prepare` instead of this, unless you are creating your own transport bypassing `mysql.connection.Connection` entirely. The prepared statement must be registered on the server BEFORE this is @@ -223,7 +223,7 @@ public: The value may, but doesn't have to be, wrapped in a Variant. If so, null is handled correctly. - + The value may, but doesn't have to be, a pointer to the desired value. The value may, but doesn't have to be, wrapped in a Nullable!T. If so, @@ -234,7 +234,7 @@ public: Parameter specializations (ie, for chunked transfer) can be added if required. If you wish to use chunked transfer (via `psn`), note that you must supply a dummy value for `val` that's typed `ubyte[]`. For example: `cast(ubyte[])[]`. - + Type_Mappings: $(TYPE_MAPPINGS) Params: index = The zero based index @@ -314,10 +314,10 @@ public: /++ Bind a tuple of D variables to the parameters of a prepared statement. - + You can use this method to bind a set of variables if you don't need any specialization, that is chunked transfer is not neccessary. - + The tuple must match the required number of parameters, and it is the programmer's responsibility to ensure that they are of appropriate types. @@ -334,10 +334,10 @@ public: /++ Bind a Variant[] as the parameters of a prepared statement. - + You can use this method to bind a set of variables in Variant form to the parameters of a prepared statement. - + Parameter specializations (ie, for chunked transfer) can be added if required. If you wish to use chunked transfer (via `psn`), note that you must supply a dummy value for `val` that's typed `ubyte[]`. For example: `cast(ubyte[])[]`. @@ -389,7 +389,7 @@ public: /++ Sets a prepared statement parameter to NULL. - + This is here mainly for legacy reasons. You can set a field to null simply by saying `prepared.setArg(index, null);` @@ -491,7 +491,7 @@ public: `a` INTEGER NOT NULL AUTO_INCREMENT, PRIMARY KEY (a) ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - + auto stmt = cn.prepare("INSERT INTO `testPreparedLastInsertID` VALUES()"); cn.exec(stmt); assert(stmt.lastInsertID == 1); @@ -547,16 +547,16 @@ package struct PreparedRegistrations(Payload) to factor out common functionality needed by both `Connection` and `MySQLPool`. +/ Payload[const(char[])] directLookup; - + /// Returns null if not found Nullable!Payload opIndex(const(char[]) sql) pure nothrow { Nullable!Payload result; - + auto pInfo = sql in directLookup; if(pInfo) result = *pInfo; - + return result; } @@ -582,7 +582,7 @@ package struct PreparedRegistrations(Payload) { setQueuedForRelease(sql, false); } - + /// Queues all prepared statements for release. void queueAllForRelease() { @@ -638,7 +638,7 @@ debug(MYSQLN_TESTS) struct TestPreparedRegistrationsBad4 { bool queuedForRelease = true; } struct TestPreparedRegistrationsGood1 { bool queuedForRelease = false; } struct TestPreparedRegistrationsGood2 { bool queuedForRelease = false; const(char)[] id; } - + static assert(!isPreparedRegistrationsPayload!int); static assert(!isPreparedRegistrationsPayload!bool); static assert(!isPreparedRegistrationsPayload!TestPreparedRegistrationsBad1); @@ -688,7 +688,7 @@ debug(MYSQLN_TESTS) assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); assert(pr["2"] == TestPreparedRegistrationsGood2(true, "2")); assert(pr["3"] == TestPreparedRegistrationsGood2(false, "3")); - + pr.queueForRelease("3"); assert(pr.directLookup.keys.length == 3); assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); @@ -733,7 +733,7 @@ debug(MYSQLN_TESTS) resetData(false, true, false); pr.clear(); assert(pr.directLookup.keys.length == 0); - + // Test registerIfNeeded auto doRegister(const(char[]) sql) { return TestPreparedRegistrationsGood2(false, sql); } pr.registerIfNeeded("1", &doRegister); diff --git a/source/mysql/protocol/comms.d b/source/mysql/protocol/comms.d index c107e25b..1549cc4e 100644 --- a/source/mysql/protocol/comms.d +++ b/source/mysql/protocol/comms.d @@ -1,4 +1,4 @@ -/++ +/++ Internal - Low-level communications. Consider this module the main entry point for the low-level MySQL/MariaDB @@ -25,12 +25,12 @@ import std.conv; import std.digest.sha; import std.exception; import std.range; -import std.variant; import mysql.connection; import mysql.exceptions; import mysql.prepared; import mysql.result; +import mysql.types; import mysql.protocol.constants; import mysql.protocol.extra_types; @@ -39,11 +39,6 @@ import mysql.protocol.packets; import mysql.protocol.sockets; -// helper for getting around calling opEquals on two typeids -private bool tideq(const TypeInfo ti1, const TypeInfo ti2) @trusted -{ - return ti1 == ti2; -} @safe: /// Low-level comms code relating to prepared statements. @@ -52,17 +47,16 @@ package struct ProtocolPrepared @safe: import std.conv; import std.datetime; - import std.variant; import mysql.types; - - static ubyte[] makeBitmap(in Variant[] inParams) + + static ubyte[] makeBitmap(in MySQLVal[] inParams) { size_t bml = (inParams.length+7)/8; ubyte[] bma; bma.length = bml; foreach (i; 0..inParams.length) { - if(!inParams[i].type.tideq(typeid(typeof(null)))) + if(inParams[i].kind != MySQLVal.Kind.Null) continue; size_t bn = i/8; size_t bb = i%8; @@ -89,7 +83,7 @@ package struct ProtocolPrepared return prefix; } - static ubyte[] analyseParams(Variant[] inParams, ParameterSpecialization[] psa, + static ubyte[] analyseParams(MySQLVal[] inParams, ParameterSpecialization[] psa, out ubyte[] vals, out bool longData) { size_t pc = inParams.length; @@ -117,112 +111,100 @@ package struct ProtocolPrepared enum SIGNED = 0; if (psa[i].chunkSize) longData= true; - if (inParams[i].type.tideq(typeid(typeof(null)))) + if (inParams[i].kind == MySQLVal.Kind.Null) { types[ct++] = SQLType.NULL; types[ct++] = SIGNED; continue; } - Variant v = inParams[i]; + MySQLVal v = inParams[i]; SQLType ext = psa[i].type; - string ts = v.type.toString(); - bool isRef; - if (ts[$-1] == '*') - { - ts.length = ts.length-1; - isRef= true; - } + auto ts = v.kind; + bool isRef = false; - switch (ts) + // TODO: use v.visit instead for more efficiency and shorter code. + with(MySQLVal.Kind) final switch (ts) { - case "bool": - case "const(bool)": - case "immutable(bool)": - case "shared(immutable(bool))": + case BitRef: + isRef = true; goto case; + case Bit: if (ext == SQLType.INFER_FROM_D_TYPE) types[ct++] = SQLType.BIT; else types[ct++] = cast(ubyte) ext; types[ct++] = SIGNED; reAlloc(2); - bool bv = isRef? *(v.get!(const(bool*))): v.get!(const(bool)); + bool bv = isRef? *v.value!BitRef : v.value!Bit; vals[vcl++] = 1; vals[vcl++] = bv? 0x31: 0x30; break; - case "byte": - case "const(byte)": - case "immutable(byte)": - case "shared(immutable(byte))": + case ByteRef: + isRef = true; goto case; + case Byte: types[ct++] = SQLType.TINY; types[ct++] = SIGNED; reAlloc(1); - vals[vcl++] = isRef? *(v.get!(const(byte*))): v.get!(const(byte)); + vals[vcl++] = isRef? *v.value!ByteRef : v.value!Byte; break; - case "ubyte": - case "const(ubyte)": - case "immutable(ubyte)": - case "shared(immutable(ubyte))": + case UByteRef: + isRef = true; goto case; + case UByte: types[ct++] = SQLType.TINY; types[ct++] = UNSIGNED; reAlloc(1); - vals[vcl++] = isRef? *(v.get!(const(ubyte*))): v.get!(const(ubyte)); + vals[vcl++] = isRef? *v.value!UByteRef : v.value!UByte; break; - case "short": - case "const(short)": - case "immutable(short)": - case "shared(immutable(short))": + case ShortRef: + isRef = true; goto case; + case Short: types[ct++] = SQLType.SHORT; types[ct++] = SIGNED; reAlloc(2); - short si = isRef? *(v.get!(const(short*))): v.get!(const(short)); + short si = isRef? *v.value!ShortRef : v.value!Short; vals[vcl++] = cast(ubyte) (si & 0xff); vals[vcl++] = cast(ubyte) ((si >> 8) & 0xff); break; - case "ushort": - case "const(ushort)": - case "immutable(ushort)": - case "shared(immutable(ushort))": + case UShortRef: + isRef = true; goto case; + case UShort: types[ct++] = SQLType.SHORT; types[ct++] = UNSIGNED; reAlloc(2); - ushort us = isRef? *(v.get!(const(ushort*))): v.get!(const(ushort)); + ushort us = isRef? *v.value!UShortRef : v.value!UShort; vals[vcl++] = cast(ubyte) (us & 0xff); vals[vcl++] = cast(ubyte) ((us >> 8) & 0xff); break; - case "int": - case "const(int)": - case "immutable(int)": - case "shared(immutable(int))": + case IntRef: + isRef = true; goto case; + case Int: types[ct++] = SQLType.INT; types[ct++] = SIGNED; reAlloc(4); - int ii = isRef? *(v.get!(const(int*))): v.get!(const(int)); + int ii = isRef? *v.value!IntRef : v.value!Int; vals[vcl++] = cast(ubyte) (ii & 0xff); vals[vcl++] = cast(ubyte) ((ii >> 8) & 0xff); vals[vcl++] = cast(ubyte) ((ii >> 16) & 0xff); vals[vcl++] = cast(ubyte) ((ii >> 24) & 0xff); break; - case "uint": - case "const(uint)": - case "immutable(uint)": - case "shared(immutable(uint))": + case UIntRef: + isRef = true; goto case; + case UInt: types[ct++] = SQLType.INT; types[ct++] = UNSIGNED; reAlloc(4); - uint ui = isRef? *(v.get!(const(uint*))): v.get!(const(uint)); + uint ui = isRef? *v.value!UIntRef : v.value!UInt; vals[vcl++] = cast(ubyte) (ui & 0xff); vals[vcl++] = cast(ubyte) ((ui >> 8) & 0xff); vals[vcl++] = cast(ubyte) ((ui >> 16) & 0xff); vals[vcl++] = cast(ubyte) ((ui >> 24) & 0xff); break; - case "long": - case "const(long)": - case "immutable(long)": - case "shared(immutable(long))": + case LongRef: + isRef = true; goto case; + case Long: types[ct++] = SQLType.LONGLONG; types[ct++] = SIGNED; reAlloc(8); - long li = isRef? *(v.get!(const(long*))): v.get!(const(long)); + long li = isRef? *v.value!LongRef : v.value!Long; vals[vcl++] = cast(ubyte) (li & 0xff); vals[vcl++] = cast(ubyte) ((li >> 8) & 0xff); vals[vcl++] = cast(ubyte) ((li >> 16) & 0xff); @@ -232,14 +214,13 @@ package struct ProtocolPrepared vals[vcl++] = cast(ubyte) ((li >> 48) & 0xff); vals[vcl++] = cast(ubyte) ((li >> 56) & 0xff); break; - case "ulong": - case "const(ulong)": - case "immutable(ulong)": - case "shared(immutable(ulong))": + case ULongRef: + isRef = true; goto case; + case ULong: types[ct++] = SQLType.LONGLONG; types[ct++] = UNSIGNED; reAlloc(8); - ulong ul = isRef? *(v.get!(const(ulong*))): v.get!(const(ulong)); + ulong ul = isRef? *v.value!ULongRef : v.value!ULong; vals[vcl++] = cast(ubyte) (ul & 0xff); vals[vcl++] = cast(ubyte) ((ul >> 8) & 0xff); vals[vcl++] = cast(ubyte) ((ul >> 16) & 0xff); @@ -249,172 +230,107 @@ package struct ProtocolPrepared vals[vcl++] = cast(ubyte) ((ul >> 48) & 0xff); vals[vcl++] = cast(ubyte) ((ul >> 56) & 0xff); break; - case "float": - case "const(float)": - case "immutable(float)": - case "shared(immutable(float))": + case FloatRef: + isRef = true; goto case; + case Float: types[ct++] = SQLType.FLOAT; types[ct++] = SIGNED; reAlloc(4); - float f = isRef? *(v.get!(const(float*))): v.get!(const(float)); - ubyte* ubp = cast(ubyte*) &f; - vals[vcl++] = *ubp++; - vals[vcl++] = *ubp++; - vals[vcl++] = *ubp++; - vals[vcl++] = *ubp; + float[1] f = [isRef? *v.value!FloatRef : v.value!Float]; + ubyte[] uba = cast(ubyte[]) f[]; + vals[vcl .. vcl + uba.length] = uba[]; + vcl += uba.length; break; - case "double": - case "const(double)": - case "immutable(double)": - case "shared(immutable(double))": + case DoubleRef: + isRef = true; goto case; + case Double: types[ct++] = SQLType.DOUBLE; types[ct++] = SIGNED; reAlloc(8); - double d = isRef? *(v.get!(const(double*))): v.get!(const(double)); - ubyte* ubp = cast(ubyte*) &d; - vals[vcl++] = *ubp++; - vals[vcl++] = *ubp++; - vals[vcl++] = *ubp++; - vals[vcl++] = *ubp++; - vals[vcl++] = *ubp++; - vals[vcl++] = *ubp++; - vals[vcl++] = *ubp++; - vals[vcl++] = *ubp; + double[1] d = [isRef? *v.value!DoubleRef : v.value!Double]; + ubyte[] uba = cast(ubyte[]) d[]; + vals[vcl .. uba.length] = uba[]; + vcl += uba.length; break; - case "std.datetime.date.Date": - case "const(std.datetime.date.Date)": - case "immutable(std.datetime.date.Date)": - case "shared(immutable(std.datetime.date.Date))": - - case "std.datetime.Date": - case "const(std.datetime.Date)": - case "immutable(std.datetime.Date)": - case "shared(immutable(std.datetime.Date))": + case DateRef: + isRef = true; goto case; + case Date: types[ct++] = SQLType.DATE; types[ct++] = SIGNED; - Date date = isRef? *(v.get!(const(Date*))): v.get!(const(Date)); + auto date = isRef? *v.value!DateRef : v.value!Date; ubyte[] da = pack(date); size_t l = da.length; reAlloc(l); vals[vcl..vcl+l] = da[]; vcl += l; break; - case "std.datetime.TimeOfDay": - case "const(std.datetime.TimeOfDay)": - case "immutable(std.datetime.TimeOfDay)": - case "shared(immutable(std.datetime.TimeOfDay))": - - case "std.datetime.date.TimeOfDay": - case "const(std.datetime.date.TimeOfDay)": - case "immutable(std.datetime.date.TimeOfDay)": - case "shared(immutable(std.datetime.date.TimeOfDay))": - - case "std.datetime.Time": - case "const(std.datetime.Time)": - case "immutable(std.datetime.Time)": - case "shared(immutable(std.datetime.Time))": + case TimeRef: + isRef = true; goto case; + case Time: types[ct++] = SQLType.TIME; types[ct++] = SIGNED; - TimeOfDay time = isRef? *(v.get!(const(TimeOfDay*))): v.get!(const(TimeOfDay)); + auto time = isRef? *v.value!TimeRef : v.value!Time; ubyte[] ta = pack(time); size_t l = ta.length; reAlloc(l); vals[vcl..vcl+l] = ta[]; vcl += l; break; - case "std.datetime.date.DateTime": - case "const(std.datetime.date.DateTime)": - case "immutable(std.datetime.date.DateTime)": - case "shared(immutable(std.datetime.date.DateTime))": - - case "std.datetime.DateTime": - case "const(std.datetime.DateTime)": - case "immutable(std.datetime.DateTime)": - case "shared(immutable(std.datetime.DateTime))": + case DateTimeRef: + isRef = true; goto case; + case DateTime: types[ct++] = SQLType.DATETIME; types[ct++] = SIGNED; - DateTime dt = isRef? *(v.get!(const(DateTime*))): v.get!(const(DateTime)); + auto dt = isRef? *v.value!DateTimeRef : v.value!DateTime; ubyte[] da = pack(dt); size_t l = da.length; reAlloc(l); vals[vcl..vcl+l] = da[]; vcl += l; break; - case "mysql.types.Timestamp": - case "const(mysql.types.Timestamp)": - case "immutable(mysql.types.Timestamp)": - case "shared(immutable(mysql.types.Timestamp))": + case TimestampRef: + isRef = true; goto case; + case Timestamp: types[ct++] = SQLType.TIMESTAMP; types[ct++] = SIGNED; - Timestamp tms = isRef? *(v.get!(const(Timestamp*))): v.get!(const(Timestamp)); - DateTime dt = mysql.protocol.packet_helpers.toDateTime(tms.rep); + auto tms = isRef? *v.value!TimestampRef : v.value!Timestamp; + auto dt = mysql.protocol.packet_helpers.toDateTime(tms.rep); ubyte[] da = pack(dt); size_t l = da.length; reAlloc(l); vals[vcl..vcl+l] = da[]; vcl += l; break; - case "char[]": - case "const(char[])": - case "immutable(char[])": - case "const(char)[]": - case "immutable(char)[]": - case "shared(immutable(char)[])": - case "shared(immutable(char))[]": - case "shared(immutable(char[]))": + case TextRef: + isRef = true; goto case; + case Text: if (ext == SQLType.INFER_FROM_D_TYPE) types[ct++] = SQLType.VARCHAR; else types[ct++] = cast(ubyte) ext; types[ct++] = SIGNED; - const char[] ca = isRef? *(v.get!(const(char[]*))): v.get!(const(char[])); - ubyte[] packed = packLCS(cast(void[]) ca); + const char[] ca = isRef? *v.value!TextRef : v.value!Text; + ubyte[] packed = packLCS(ca); reAlloc(packed.length); vals[vcl..vcl+packed.length] = packed[]; vcl += packed.length; break; - case "byte[]": - case "const(byte[])": - case "immutable(byte[])": - case "const(byte)[]": - case "immutable(byte)[]": - case "shared(immutable(byte)[])": - case "shared(immutable(byte))[]": - case "shared(immutable(byte[]))": + case BlobRef: + isRef = true; goto case; + case Blob: if (ext == SQLType.INFER_FROM_D_TYPE) types[ct++] = SQLType.TINYBLOB; else types[ct++] = cast(ubyte) ext; types[ct++] = SIGNED; - const byte[] ba = isRef? *(v.get!(const(byte[]*))): v.get!(const(byte[])); - ubyte[] packed = packLCS(cast(void[]) ba); + const ubyte[] uba = isRef? *v.value!BlobRef : v.value!Blob; + ubyte[] packed = packLCS(uba); reAlloc(packed.length); vals[vcl..vcl+packed.length] = packed[]; vcl += packed.length; break; - case "ubyte[]": - case "const(ubyte[])": - case "immutable(ubyte[])": - case "const(ubyte)[]": - case "immutable(ubyte)[]": - case "shared(immutable(ubyte)[])": - case "shared(immutable(ubyte))[]": - case "shared(immutable(ubyte[]))": - if (ext == SQLType.INFER_FROM_D_TYPE) - types[ct++] = SQLType.TINYBLOB; - else - types[ct++] = cast(ubyte) ext; - types[ct++] = SIGNED; - const ubyte[] uba = isRef? *(v.get!(const(ubyte[]*))): v.get!(const(ubyte[])); - ubyte[] packed = packLCS(cast(void[]) uba); - reAlloc(packed.length); - vals[vcl..vcl+packed.length] = packed[]; - vcl += packed.length; - break; - case "void": + case Null: throw new MYX("Unbound parameter " ~ to!string(i), __FILE__, __LINE__); - default: - throw new MYX("Unsupported parameter type " ~ ts, __FILE__, __LINE__); } } vals.length = vcl; @@ -428,7 +344,7 @@ package struct ProtocolPrepared { if (!psn.chunkSize) continue; uint cs = psn.chunkSize; - uint delegate(ubyte[]) dg = psn.chunkDelegate; + uint delegate(ubyte[]) @safe dg = psn.chunkDelegate; ubyte[] chunk; chunk.length = cs+11; @@ -457,10 +373,10 @@ package struct ProtocolPrepared } static void sendCommand(Connection conn, uint hStmt, PreparedStmtHeaders psh, - Variant[] inParams, ParameterSpecialization[] psa) + MySQLVal[] inParams, ParameterSpecialization[] psa) { conn.autoPurge(); - + ubyte[] packet; conn.resetPacket(); @@ -499,7 +415,7 @@ package(mysql) struct ExecQueryImplInfo // For prepared statements: uint hStmt; PreparedStmtHeaders psh; - Variant[] inParams; + MySQLVal[] inParams; ParameterSpecialization[] psa; } @@ -645,7 +561,7 @@ body // Moved here from `struct Row.this` package(mysql) void ctorRow(Connection conn, ref ubyte[] packet, ResultSetHeaders rh, bool binary, - out Variant[] _values, out bool[] _nulls, out string[] _names) + out MySQLVal[] _values, out bool[] _nulls, out string[] _names) in { assert(rh.fieldCount <= uint.max); @@ -780,7 +696,7 @@ body } conn.autoPurge(); - + conn.resetPacket(); ubyte[] header; @@ -976,7 +892,7 @@ package(mysql) SvrCapFlags setClientFlags(SvrCapFlags serverCaps, SvrCapFlags ca // didn't supply it cCaps |= SvrCapFlags.PROTOCOL41; cCaps |= SvrCapFlags.SECURE_CONNECTION; - + return cCaps; } @@ -1007,7 +923,7 @@ package(mysql) PreparedServerInfo performRegister(Connection conn, const(char[]) scope(failure) conn.kill(); PreparedServerInfo info; - + conn.sendCmd(CommandType.STMT_PREPARE, sql); conn._fieldCount = 0; diff --git a/source/mysql/protocol/packet_helpers.d b/source/mysql/protocol/packet_helpers.d index aacd313e..7bc0b88c 100644 --- a/source/mysql/protocol/packet_helpers.d +++ b/source/mysql/protocol/packet_helpers.d @@ -1,4 +1,4 @@ -/// Internal - Helper functions for the communication protocol. +/// Internal - Helper functions for the communication protocol. module mysql.protocol.packet_helpers; import std.algorithm; @@ -998,12 +998,12 @@ body return t; } -ubyte[] packLCS(void[] a) pure nothrow +ubyte[] packLCS(const(void)[] a) pure nothrow { size_t offset; ubyte[] t = packLength(a.length, offset); if (t[0]) - t[offset..$] = (cast(ubyte[]) a)[0..$]; + t[offset..$] = (cast(const(ubyte)[]) a)[0..$]; return t; } diff --git a/source/mysql/types.d b/source/mysql/types.d index dc247223..1b49564e 100644 --- a/source/mysql/types.d +++ b/source/mysql/types.d @@ -1,5 +1,7 @@ /// Structures for MySQL types not built-in to D/Phobos. module mysql.types; +import taggedalgebraic.taggedunion; +import std.datetime : DateTime, TimeOfDay, Date; /++ A simple struct to represent time difference. @@ -29,3 +31,46 @@ struct Timestamp { ulong rep; } + +union _MYTYPE +{ + typeof(null) Null; + bool Bit; + ubyte UByte; + byte Byte; + ushort UShort; + short Short; + uint UInt; + int Int; + ulong ULong; + long Long; + float Float; + double Double; + .DateTime DateTime; + TimeOfDay Time; + .Timestamp Timestamp; + .Date Date; + string Text; + ubyte[] Blob; + + // pointers + const(bool)* BitRef; + const(ubyte)* UByteRef; + const(byte)* ByteRef; + const(ushort)* UShortRef; + const(short)* ShortRef; + const(uint)* UIntRef; + const(int)* IntRef; + const(ulong)* ULongRef; + const(long)* LongRef; + const(float)* FloatRef; + const(double)* DoubleRef; + const(.DateTime)* DateTimeRef; + const(TimeOfDay)* TimeRef; + const(.Date)* DateRef; + const(string)* TextRef; + const(ubyte[])* BlobRef; + const(.Timestamp)* TimestampRef; +} + +alias MySQLVal = TaggedUnion!_MYTYPE; From 40f9179cd69b7453d17cd13e6ba98724423e19d3 Mon Sep 17 00:00:00 2001 From: Steven Schveighoffer Date: Thu, 28 Nov 2019 19:54:32 -0500 Subject: [PATCH 03/14] Building version of @safe mysql-native. Needs some more work, will squash --- dub.sdl | 2 +- dub.selections.json | 4 +- source/mysql/commands.d | 6 +- source/mysql/prepared.d | 90 +++++++++++++++----------- source/mysql/protocol/comms.d | 52 ++++++++------- source/mysql/protocol/extra_types.d | 7 +- source/mysql/protocol/packet_helpers.d | 43 +++++------- source/mysql/protocol/packets.d | 6 ++ source/mysql/result.d | 53 +++++++++++---- source/mysql/types.d | 60 ++++++++++++++++- 10 files changed, 217 insertions(+), 106 deletions(-) diff --git a/dub.sdl b/dub.sdl index 8d698b25..9ffaf559 100644 --- a/dub.sdl +++ b/dub.sdl @@ -5,7 +5,7 @@ copyright "Copyright (c) 2011-2019 Steve Teale, James W. Oliphant, Simen Endsj authors "Steve Teale" "James W. Oliphant" "Simen Endsjø" "Sönke Ludwig" "Sergey Shamov" "Nick Sabalausky" dependency "vibe-core" version="~>1.0" optional=true -dependency "taggedalgebraic" version="~>0.11.6" +dependency "taggedalgebraic" version="~>0.11.7" sourcePaths "source/" importPaths "source/" diff --git a/dub.selections.json b/dub.selections.json index c849738f..1a2f7fc7 100644 --- a/dub.selections.json +++ b/dub.selections.json @@ -8,8 +8,8 @@ "memutils": "0.4.13", "openssl": "1.1.4+1.0.1g", "stdx-allocator": "2.77.5", - "taggedalgebraic": "0.11.6", - "unit-threaded": "0.7.45", + "taggedalgebraic": "0.11.7", + "unit-threaded": "0.7.55", "vibe-core": "1.7.0" } } diff --git a/source/mysql/commands.d b/source/mysql/commands.d index b5a3aadd..7dd5bce8 100644 --- a/source/mysql/commands.d +++ b/source/mysql/commands.d @@ -46,7 +46,7 @@ struct ColumnSpecialization size_t cIndex; // parameter number 0 - number of params-1 ushort type; uint chunkSize; /// In bytes - void delegate(const(ubyte)[] chunk, bool finished) chunkDelegate; + void delegate(const(ubyte)[] chunk, bool finished) @safe chunkDelegate; } ///ditto alias CSN = ColumnSpecialization; @@ -76,7 +76,7 @@ unittest immutable selectSQL = "SELECT `data` FROM `columnSpecial`"; ubyte[] received; bool lastValueOfFinished; - void receiver(const(ubyte)[] chunk, bool finished) + void receiver(const(ubyte)[] chunk, bool finished) @safe { assert(lastValueOfFinished == false); @@ -381,7 +381,7 @@ package ResultRange queryImpl(ColumnSpecialization[] csa, conn._rsh.addSpecializations(csa); conn._headersPending = false; - return ResultRange(conn, conn._rsh, conn._rsh.fieldNames); + return ResultRange(SafeResultRange(conn, conn._rsh, conn._rsh.fieldNames)); } /++ diff --git a/source/mysql/prepared.d b/source/mysql/prepared.d index f4225b49..d8ec6ca4 100644 --- a/source/mysql/prepared.d +++ b/source/mysql/prepared.d @@ -13,6 +13,7 @@ import mysql.protocol.comms; import mysql.protocol.constants; import mysql.protocol.packets; import mysql.result; +import mysql.types; debug(MYSQLN_TESTS) import mysql.test.common; @@ -133,20 +134,6 @@ unittest } } -// temporary trait fix to allow setting/getting of @safe types from args. -private template isSafeType(T) -{ - U foo(U)(U t) { - static if(is(typeof(t = t))) - t = t; - return t; - } - - enum isSafeType = is(typeof(() @safe => foo(T.init))); -} -static assert(isSafeType!(int)); -static assert(isSafeType!(typeof(null))); - /++ Encapsulation of a prepared statement. @@ -168,7 +155,7 @@ private: package: ushort _numParams; /// Number of parameters this prepared statement takes PreparedStmtHeaders _headers; - Variant[] _inParams; + MySQLVal[] _inParams; ParameterSpecialization[] _psa; ColumnSpecialization[] _columnSpecials; ulong _lastInsertID; @@ -205,23 +192,15 @@ public: _psa.length = numParams; } - private void _assignArgImpl(T)(size_t index, ref T val) + private void _assignArgImpl(T)(size_t index, auto ref T val) { - static if(isSafeType!T) - { - // override Variant's unsafe mechanism, we know everything is safe. - (() @trusted { _inParams[index] = val; })(); - } - else - { - _inParams[index] = val; - } + _inParams[index] = val; } /++ Prepared statement parameter setter. - The value may, but doesn't have to be, wrapped in a Variant. If so, + The value may, but doesn't have to be, wrapped in a MySQLVal. If so, null is handled correctly. The value may, but doesn't have to be, a pointer to the desired value. @@ -240,7 +219,7 @@ public: Params: index = The zero based index +/ void setArg(T)(size_t index, T val, ParameterSpecialization psn = PSN(0, SQLType.INFER_FROM_D_TYPE, 0, null)) - if(!isInstanceOf!(Nullable, T)) + if(!isInstanceOf!(Nullable, T) && !is(T == Variant)) { // Now in theory we should be able to check the parameter type here, since the // protocol is supposed to send us type information for the parameters, but this @@ -266,6 +245,17 @@ public: setArg(index, val.get(), psn); } + deprecated("Using Variant is deprecated, please use MySQLVal instead") + void setArg(T)(size_t index, T val, ParameterSpecialization psn = PSN(0, SQLType.INFER_FROM_D_TYPE, 0, null)) + if(is(T == Variant)) + { + enforce!MYX(index < _numParams, "Parameter index out of range."); + + _assignArgImpl(index, _toVal(val)); + psn.pIndex = index; + _psa[index] = psn; + } + @("setArg-typeMods") debug(MYSQLN_TESTS) unittest @@ -305,11 +295,13 @@ public: // Note: Variant doesn't seem to support // `shared(T)` or `shared(const(T)`. Only `shared(immutable(T))`. + // Further note, shared immutable(int) is really + // immutable(int). This test is a duplicate, so removed. // Test shared immutable(int) - { + /*{ shared immutable(int) i = 113; assert(cn.exec(insertSQL, i) == 1); - } + }*/ } /++ @@ -324,7 +316,7 @@ public: Type_Mappings: $(TYPE_MAPPINGS) +/ void setArgs(T...)(T args) - if(T.length == 0 || !is(T[0] == Variant[])) + if(T.length == 0 || (!is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]))) { enforce!MYX(args.length == _numParams, "Argument list supplied does not match the number of parameters."); @@ -333,9 +325,9 @@ public: } /++ - Bind a Variant[] as the parameters of a prepared statement. + Bind a MySQLVal[] as the parameters of a prepared statement. - You can use this method to bind a set of variables in Variant form to + You can use this method to bind a set of variables in MySQLVal form to the parameters of a prepared statement. Parameter specializations (ie, for chunked transfer) can be added if required. @@ -359,13 +351,12 @@ public: Type_Mappings: $(TYPE_MAPPINGS) Params: - args = External list of Variants to be used as parameters + args = External list of MySQLVal to be used as parameters psnList = Any required specializations +/ - void setArgs(Variant[] args, ParameterSpecialization[] psnList=null) + void setArgs(MySQLVal[] args, ParameterSpecialization[] psnList=null) @safe { enforce!MYX(args.length == _numParams, "Param count supplied does not match prepared statement"); - // can't be @safe because of the postblit for Variant _inParams[] = args[]; if (psnList !is null) { @@ -374,14 +365,41 @@ public: } } + /// ditto + deprecated("Using Variant is deprecated, please use MySQLVal instead") + void setArgs(Variant[] args, ParameterSpecialization[] psnList=null) + { + enforce!MYX(args.length == _numParams, "Param count supplied does not match prepared statement"); + foreach(i, ref arg; args) + setArg(i, arg); + if (psnList !is null) + { + foreach (PSN psn; psnList) + _psa[psn.pIndex] = psn; + } + } + /++ Prepared statement parameter getter. Type_Mappings: $(TYPE_MAPPINGS) Params: index = The zero based index + + Note: The Variant version of this function, getArg, is deprecated. + safeGetArg will eventually be renamed getArg when it is removed. +/ + deprecated("Using Variant is deprecated, please use safeGetArg instead") Variant getArg(size_t index) + { + enforce!MYX(index < _numParams, "Parameter index out of range."); + + // convert to Variant. + return _toVar(_inParams[index]); + } + + /// ditto + MySQLVal safeGetArg(size_t index) @safe { enforce!MYX(index < _numParams, "Parameter index out of range."); return _inParams[index]; @@ -458,7 +476,7 @@ public: assert(rs[1].isNull(0)); assert(rs[1][0].type == typeid(typeof(null))); - preparedInsert.setArg(0, Variant(null)); + preparedInsert.setArg(0, MySQLVal(null)); cn.exec(preparedInsert); rs = cn.query(selectSQL).array; assert(rs.length == 3); diff --git a/source/mysql/protocol/comms.d b/source/mysql/protocol/comms.d index 1549cc4e..c1305286 100644 --- a/source/mysql/protocol/comms.d +++ b/source/mysql/protocol/comms.d @@ -38,6 +38,14 @@ import mysql.protocol.packet_helpers; import mysql.protocol.packets; import mysql.protocol.sockets; +/** Gets the value stored in an algebraic type based on its data type. +*/ +import taggedalgebraic.taggedalgebraic; +auto ref get(alias K, U)(auto ref TaggedAlgebraic!U ta) if (is(typeof(K) == TaggedAlgebraic!U.Kind)) +{ + import taggedalgebraic.taggedunion; + return (cast(TaggedUnion!U)ta).value!K; +} @safe: @@ -134,7 +142,7 @@ package struct ProtocolPrepared types[ct++] = cast(ubyte) ext; types[ct++] = SIGNED; reAlloc(2); - bool bv = isRef? *v.value!BitRef : v.value!Bit; + bool bv = isRef? *v.get!BitRef : v.get!Bit; vals[vcl++] = 1; vals[vcl++] = bv? 0x31: 0x30; break; @@ -144,7 +152,7 @@ package struct ProtocolPrepared types[ct++] = SQLType.TINY; types[ct++] = SIGNED; reAlloc(1); - vals[vcl++] = isRef? *v.value!ByteRef : v.value!Byte; + vals[vcl++] = isRef? *v.get!ByteRef : v.get!Byte; break; case UByteRef: isRef = true; goto case; @@ -152,7 +160,7 @@ package struct ProtocolPrepared types[ct++] = SQLType.TINY; types[ct++] = UNSIGNED; reAlloc(1); - vals[vcl++] = isRef? *v.value!UByteRef : v.value!UByte; + vals[vcl++] = isRef? *v.get!UByteRef : v.get!UByte; break; case ShortRef: isRef = true; goto case; @@ -160,7 +168,7 @@ package struct ProtocolPrepared types[ct++] = SQLType.SHORT; types[ct++] = SIGNED; reAlloc(2); - short si = isRef? *v.value!ShortRef : v.value!Short; + short si = isRef? *v.get!ShortRef : v.get!Short; vals[vcl++] = cast(ubyte) (si & 0xff); vals[vcl++] = cast(ubyte) ((si >> 8) & 0xff); break; @@ -170,7 +178,7 @@ package struct ProtocolPrepared types[ct++] = SQLType.SHORT; types[ct++] = UNSIGNED; reAlloc(2); - ushort us = isRef? *v.value!UShortRef : v.value!UShort; + ushort us = isRef? *v.get!UShortRef : v.get!UShort; vals[vcl++] = cast(ubyte) (us & 0xff); vals[vcl++] = cast(ubyte) ((us >> 8) & 0xff); break; @@ -180,7 +188,7 @@ package struct ProtocolPrepared types[ct++] = SQLType.INT; types[ct++] = SIGNED; reAlloc(4); - int ii = isRef? *v.value!IntRef : v.value!Int; + int ii = isRef? *v.get!IntRef : v.get!Int; vals[vcl++] = cast(ubyte) (ii & 0xff); vals[vcl++] = cast(ubyte) ((ii >> 8) & 0xff); vals[vcl++] = cast(ubyte) ((ii >> 16) & 0xff); @@ -192,7 +200,7 @@ package struct ProtocolPrepared types[ct++] = SQLType.INT; types[ct++] = UNSIGNED; reAlloc(4); - uint ui = isRef? *v.value!UIntRef : v.value!UInt; + uint ui = isRef? *v.get!UIntRef : v.get!UInt; vals[vcl++] = cast(ubyte) (ui & 0xff); vals[vcl++] = cast(ubyte) ((ui >> 8) & 0xff); vals[vcl++] = cast(ubyte) ((ui >> 16) & 0xff); @@ -204,7 +212,7 @@ package struct ProtocolPrepared types[ct++] = SQLType.LONGLONG; types[ct++] = SIGNED; reAlloc(8); - long li = isRef? *v.value!LongRef : v.value!Long; + long li = isRef? *v.get!LongRef : v.get!Long; vals[vcl++] = cast(ubyte) (li & 0xff); vals[vcl++] = cast(ubyte) ((li >> 8) & 0xff); vals[vcl++] = cast(ubyte) ((li >> 16) & 0xff); @@ -220,7 +228,7 @@ package struct ProtocolPrepared types[ct++] = SQLType.LONGLONG; types[ct++] = UNSIGNED; reAlloc(8); - ulong ul = isRef? *v.value!ULongRef : v.value!ULong; + ulong ul = isRef? *v.get!ULongRef : v.get!ULong; vals[vcl++] = cast(ubyte) (ul & 0xff); vals[vcl++] = cast(ubyte) ((ul >> 8) & 0xff); vals[vcl++] = cast(ubyte) ((ul >> 16) & 0xff); @@ -236,7 +244,7 @@ package struct ProtocolPrepared types[ct++] = SQLType.FLOAT; types[ct++] = SIGNED; reAlloc(4); - float[1] f = [isRef? *v.value!FloatRef : v.value!Float]; + float[1] f = [isRef? *v.get!FloatRef : v.get!Float]; ubyte[] uba = cast(ubyte[]) f[]; vals[vcl .. vcl + uba.length] = uba[]; vcl += uba.length; @@ -247,7 +255,7 @@ package struct ProtocolPrepared types[ct++] = SQLType.DOUBLE; types[ct++] = SIGNED; reAlloc(8); - double[1] d = [isRef? *v.value!DoubleRef : v.value!Double]; + double[1] d = [isRef? *v.get!DoubleRef : v.get!Double]; ubyte[] uba = cast(ubyte[]) d[]; vals[vcl .. uba.length] = uba[]; vcl += uba.length; @@ -257,7 +265,7 @@ package struct ProtocolPrepared case Date: types[ct++] = SQLType.DATE; types[ct++] = SIGNED; - auto date = isRef? *v.value!DateRef : v.value!Date; + auto date = isRef? *v.get!DateRef : v.get!Date; ubyte[] da = pack(date); size_t l = da.length; reAlloc(l); @@ -269,7 +277,7 @@ package struct ProtocolPrepared case Time: types[ct++] = SQLType.TIME; types[ct++] = SIGNED; - auto time = isRef? *v.value!TimeRef : v.value!Time; + auto time = isRef? *v.get!TimeRef : v.get!Time; ubyte[] ta = pack(time); size_t l = ta.length; reAlloc(l); @@ -281,7 +289,7 @@ package struct ProtocolPrepared case DateTime: types[ct++] = SQLType.DATETIME; types[ct++] = SIGNED; - auto dt = isRef? *v.value!DateTimeRef : v.value!DateTime; + auto dt = isRef? *v.get!DateTimeRef : v.get!DateTime; ubyte[] da = pack(dt); size_t l = da.length; reAlloc(l); @@ -293,7 +301,7 @@ package struct ProtocolPrepared case Timestamp: types[ct++] = SQLType.TIMESTAMP; types[ct++] = SIGNED; - auto tms = isRef? *v.value!TimestampRef : v.value!Timestamp; + auto tms = isRef? *v.get!TimestampRef : v.get!Timestamp; auto dt = mysql.protocol.packet_helpers.toDateTime(tms.rep); ubyte[] da = pack(dt); size_t l = da.length; @@ -309,7 +317,7 @@ package struct ProtocolPrepared else types[ct++] = cast(ubyte) ext; types[ct++] = SIGNED; - const char[] ca = isRef? *v.value!TextRef : v.value!Text; + const char[] ca = isRef? *v.get!TextRef : v.get!Text; ubyte[] packed = packLCS(ca); reAlloc(packed.length); vals[vcl..vcl+packed.length] = packed[]; @@ -323,7 +331,7 @@ package struct ProtocolPrepared else types[ct++] = cast(ubyte) ext; types[ct++] = SIGNED; - const ubyte[] uba = isRef? *v.value!BlobRef : v.value!Blob; + const ubyte[] uba = isRef? *v.get!BlobRef : v.get!Blob; ubyte[] packed = packLCS(uba); reAlloc(packed.length); vals[vcl..vcl+packed.length] = packed[]; @@ -785,7 +793,7 @@ package(mysql) ubyte[] makeToken(string password, ubyte[] authBuf) } /// Get the next `mysql.result.Row` of a pending result set. -package(mysql) Row getNextRow(Connection conn) +package(mysql) SafeRow getNextRow(Connection conn) { scope(failure) conn.kill(); @@ -795,7 +803,7 @@ package(mysql) Row getNextRow(Connection conn) conn._headersPending = false; } ubyte[] packet; - Row rr; + SafeRow rr; packet = conn.getPacket(); if(packet.front == ResultPacketMarker.error) throw new MYXReceived(OKErrorPacket(packet), __FILE__, __LINE__); @@ -806,9 +814,9 @@ package(mysql) Row getNextRow(Connection conn) return rr; } if (conn._binaryPending) - rr = Row(conn, packet, conn._rsh, true); + rr = SafeRow(conn, packet, conn._rsh, true); else - rr = Row(conn, packet, conn._rsh, false); + rr = SafeRow(conn, packet, conn._rsh, false); //rr._valid = true; return rr; } @@ -1007,7 +1015,7 @@ Get a textual report on the server status. (COM_STATISTICS) +/ -package(mysql) string serverStats(Connection conn) +package(mysql) string serverStats(Connection conn) @trusted { conn.sendCmd(CommandType.STATISTICS, []); return cast(string) conn.getPacket(); diff --git a/source/mysql/protocol/extra_types.d b/source/mysql/protocol/extra_types.d index c1cb2910..be4c59d1 100644 --- a/source/mysql/protocol/extra_types.d +++ b/source/mysql/protocol/extra_types.d @@ -8,15 +8,17 @@ import mysql.commands; import mysql.exceptions; import mysql.protocol.sockets; import mysql.result; +import mysql.types; struct SQLValue { bool isNull; bool isIncomplete; - Variant _value; + MySQLVal _value; + @safe: // empty template as a template and non-template won't be added to the same overload set - @property inout(Variant) value()() inout + @property inout(MySQLVal) value()() inout { enforce!MYX(!isNull, "SQL value is null"); enforce!MYX(!isIncomplete, "SQL value not complete"); @@ -41,6 +43,7 @@ struct SQLValue /// Length Coded Binary Value struct LCB { + @safe: /// True if the `LCB` contains a null value bool isNull; diff --git a/source/mysql/protocol/packet_helpers.d b/source/mysql/protocol/packet_helpers.d index 7bc0b88c..f64a9d9b 100644 --- a/source/mysql/protocol/packet_helpers.d +++ b/source/mysql/protocol/packet_helpers.d @@ -107,7 +107,7 @@ Text representations of a time of day are as in 14:22:02 Params: s = A string representation of the time. Returns: A populated or default initialized std.datetime.TimeOfDay struct. +/ -TimeOfDay toTimeOfDay(string s) +TimeOfDay toTimeOfDay(const(char)[] s) { TimeOfDay tod; tod.hour = parse!int(s); @@ -178,7 +178,7 @@ Text representations of a Date are as in 2011-11-11 Params: s = A string representation of the time difference. Returns: A populated or default initialized `std.datetime.Date` struct. +/ -Date toDate(string s) +Date toDate(const(char)[] s) { int year = parse!(ushort)(s); enforce!MYXProtocol(s.skipOver("-"), `Expected: "-"`); @@ -261,7 +261,7 @@ Text representations of a DateTime are as in 2011-11-11 12:20:02 Params: s = A string representation of the time difference. Returns: A populated or default initialized `std.datetime.DateTime` struct. +/ -DateTime toDateTime(string s) +DateTime toDateTime(const(char)[] s) { int year = parse!(ushort)(s); enforce!MYXProtocol(s.skipOver("-"), `Expected: "-"`); @@ -358,7 +358,8 @@ in } body { - return cast(string)packet.consume(N); + auto result = packet.consume(N); + return (() @trusted => cast(string)result)(); } /// Returns N number of bytes from the packet and advances the array @@ -531,7 +532,7 @@ body } -T myto(T)(string value) +T myto(T)(const(char)[] value) { static if(is(T == DateTime)) return toDateTime(value); @@ -552,9 +553,14 @@ in } body { - T result = 0; - (cast(ubyte*)&result)[0..T.sizeof] = packet[0..T.sizeof]; - return result; + union R { + T val = 0; + ubyte[T.sizeof] bytes; + } + + R item; + item.bytes[] = packet[0..T.sizeof]; + return item.val; } T consume(T, ubyte N=T.sizeof)(ref ubyte[] packet) pure nothrow @@ -614,7 +620,7 @@ SQLValue consumeNonBinaryValueIfComplete(T)(ref ubyte[] packet, bool unsigned) // and convert the data packet.skip(lcb.totalBytes); assert(packet.length >= lcb.value); - auto value = cast(string) packet.consume(cast(size_t)lcb.value); + auto value = cast(char[]) packet.consume(cast(size_t)lcb.value); if(!result.isNull) { @@ -629,22 +635,7 @@ SQLValue consumeNonBinaryValueIfComplete(T)(ref ubyte[] packet, bool unsigned) } else { - static if(isArray!T) - { - // to!() crashes when trying to convert empty strings - // to arrays, so we have this hack to just store any - // empty array in those cases - if(!value.length) - result.value = T.init; - else - result.value = cast(T)value.dup; - - } - else - { - // TODO: DateTime values etc might be incomplete! - result.value = myto!T(value); - } + result.value = value.myto!T; } } } @@ -726,7 +717,7 @@ SQLValue consumeIfComplete()(ref ubyte[] packet, SQLType sqlType, bool binary, b if(charSet == 0x3F) // CharacterSet == binary result.value = data; // BLOB-ish else - result.value = cast(string)data; // TEXT-ish + result.value = (() @trusted => cast(string)data)(); // TEXT-ish } // Type BIT is treated as a length coded binary (like a BLOB or VARCHAR), diff --git a/source/mysql/protocol/packets.d b/source/mysql/protocol/packets.d index 920ca41f..e6544f4f 100644 --- a/source/mysql/protocol/packets.d +++ b/source/mysql/protocol/packets.d @@ -29,6 +29,7 @@ See_Also: $(LINK http://forge.mysql.com/wiki/MySQL_Internals_ClientServer_Protoc +/ struct OKErrorPacket { + @safe: bool error; ulong affected; ulong insertID; @@ -95,6 +96,7 @@ See_Also: $(LINK http://forge.mysql.com/wiki/MySQL_Internals_ClientServer_Protoc +/ struct FieldDescription { + @safe: private: string _db; string _table; @@ -219,6 +221,7 @@ packets is sent, but they contain no useful information and are all the same. +/ struct ParamDescription { + @safe: private: ushort _type; FieldFlags _flags; @@ -266,6 +269,7 @@ See_Also: $(LINK http://forge.mysql.com/wiki/MySQL_Internals_ClientServer_Protoc +/ struct EOFPacket { + @safe: private: ushort _warnings; ushort _serverStatus; @@ -312,6 +316,7 @@ before the row data packets can be read. +/ struct ResultSetHeaders { + @safe: import mysql.connection; private: @@ -401,6 +406,7 @@ As noted in `ParamDescription` description, parameter descriptions are not fully +/ struct PreparedStmtHeaders { + @safe: import mysql.connection; package: diff --git a/source/mysql/result.d b/source/mysql/result.d index be9a5555..3a15a35e 100644 --- a/source/mysql/result.d +++ b/source/mysql/result.d @@ -12,6 +12,7 @@ import mysql.exceptions; import mysql.protocol.comms; import mysql.protocol.extra_types; import mysql.protocol.packets; +import mysql.types; /++ A struct to represent a single row of a result set. @@ -27,17 +28,18 @@ I have been agitating for some kind of null indicator that can be set for a Variant without destroying its inherent type information. If this were the case, then the bool array could disappear. +/ -struct Row +struct SafeRow { import mysql.connection; package: - Variant[] _values; // Temporarily "package" instead of "private" + MySQLVal[] _values; // Temporarily "package" instead of "private" private: bool[] _nulls; string[] _names; public: + @safe: /++ A constructor to extract the column data from a row data packet. @@ -66,7 +68,7 @@ public: Params: i = the zero based index of the column whose value is required. Returns: A Variant holding the column value. +/ - inout(Variant) opIndex(size_t i) inout + inout(MySQLVal) opIndex(size_t i) inout { enforce!MYX(_nulls.length > 0, format("Cannot get column index %d. There are no columns", i)); enforce!MYX(i < _nulls.length, format("Cannot get column index %d. The last available index is %d", i, _nulls.length-1)); @@ -170,12 +172,21 @@ public: { import std.stdio; - foreach(Variant v; _values) - writef("%s, ", v.toString()); - writeln(""); + writefln("%(%s, %)", _values); } } +/// ditto +struct Row +{ + SafeRow safe; + alias safe this; + deprecated("Variant support is deprecated. Please use SafeRow instead of Row.") + Variant opIndex(size_t idx) const { + return _toVar(safe[idx]); + } +} + /++ An $(LINK2 http://dlang.org/phobos/std_range_primitives.html#isInputRange, input range) of Row. @@ -206,12 +217,13 @@ ResultRange oneAtATime = myConnection.query("SELECT * from myTable"); Row[] allAtOnce = myConnection.query("SELECT * from myTable").array; --- +/ -struct ResultRange +struct SafeResultRange { private: +@safe: Connection _con; ResultSetHeaders _rsh; - Row _row; // current row + SafeRow _row; // current row string[] _colNames; size_t[string] _colNameIndicies; ulong _numRowsFetched; @@ -261,7 +273,7 @@ public: /++ Gets the current row +/ - @property inout(Row) front() pure inout + @property inout(SafeRow) front() pure inout { ensureValid(); enforce!MYX(!empty, "Attempted 'front' on exhausted result sequence."); @@ -284,11 +296,11 @@ public: Type_Mappings: $(TYPE_MAPPINGS) +/ - Variant[string] asAA() + MySQLVal[string] asAA() { ensureValid(); enforce!MYX(!empty, "Attempted 'front' on exhausted result sequence."); - Variant[string] aa; + MySQLVal[string] aa; foreach (size_t i, string s; _colNames) aa[s] = _row._values[i]; return aa; @@ -325,3 +337,22 @@ public: +/ @property ulong rowCount() const pure nothrow { return _numRowsFetched; } } + +struct ResultRange +{ + SafeResultRange safe; + alias safe this; + deprecated("Usage of Variant is deprecated. Use SafeResultRange instead of ResultRange") + inout(Row) front() inout { return inout(Row)(safe.front); } + + deprecated("Usage of Variant is deprecated. Use SafeResultRange instead of ResultRange") + Variant[string] asAA() + { + ensureValid(); + enforce!MYX(!safe.empty, "Attempted 'front' on exhausted result sequence."); + Variant[string] aa; + foreach (size_t i, string s; _colNames) + aa[s] = _toVar(_row._values[i]); + return aa; + } +} diff --git a/source/mysql/types.d b/source/mysql/types.d index 1b49564e..415543df 100644 --- a/source/mysql/types.d +++ b/source/mysql/types.d @@ -1,6 +1,6 @@ /// Structures for MySQL types not built-in to D/Phobos. module mysql.types; -import taggedalgebraic.taggedunion; +import taggedalgebraic.taggedalgebraic; import std.datetime : DateTime, TimeOfDay, Date; /++ @@ -34,6 +34,12 @@ struct Timestamp union _MYTYPE { + // blobs are const because of the indirection. In this case, it's not + // important because nobody is going to use MySQLVal to maintain their + // ubyte array. + const(ubyte)[] Blob; + +@disableIndex: // do not want indexing on anything other than blobs. typeof(null) Null; bool Bit; ubyte UByte; @@ -50,8 +56,8 @@ union _MYTYPE TimeOfDay Time; .Timestamp Timestamp; .Date Date; + string Text; - ubyte[] Blob; // pointers const(bool)* BitRef; @@ -73,4 +79,52 @@ union _MYTYPE const(.Timestamp)* TimestampRef; } -alias MySQLVal = TaggedUnion!_MYTYPE; +alias MySQLVal = TaggedAlgebraic!_MYTYPE; + +// helper to convert variants to MySQLVal. Used wherever variant is still used. +import std.variant : Variant; +package MySQLVal _toVal(Variant v) +{ + // unfortunately, we need to use a giant switch. But hopefully people will stop using Variant, and this will go away. + string ts = v.type.toString(); + bool isRef; + if (ts[$-1] == '*') + { + ts.length = ts.length-1; + isRef= true; + } + + import std.meta; + import std.traits; + import mysql.exceptions; + alias AllTypes = AliasSeq!(bool, byte, ubyte, short, ushort, int, uint, long, ulong, float, double, DateTime, TimeOfDay, Date, string, ubyte[], Timestamp); + switch (ts) + { + static foreach(Type; AllTypes) + { + case fullyQualifiedName!Type: + case "const(" ~ fullyQualifiedName!Type ~ ")": + case "immutable(" ~ fullyQualifiedName!Type ~ ")": + case "shared(immutable(" ~ fullyQualifiedName!Type ~ "))": + if(isRef) + return MySQLVal(v.get!(const(Type*))); + else + return MySQLVal(v.get!(const(Type))); + } + default: + throw new MYX("Unsupported Database Variant Type: " ~ ts); + } +} + +// convert MySQLVal to variant. Will eventually be removed when Variant support +// is removed. +package Variant _toVar(MySQLVal v) +{ + return v.apply!((a) => Variant(a)); +} + +// helper to fix deficiency of convertsTo in TaggedAlgebraic +package bool convertsTo(T)(ref MySQLVal val) +{ + return v.apply!((a) => is(typeof(a) : T)); +} From 1a475fd9e5bc03879bf1fd65cd1a26eb2b41f7f5 Mon Sep 17 00:00:00 2001 From: Steven Schveighoffer Date: Thu, 28 Nov 2019 20:14:44 -0500 Subject: [PATCH 04/14] Fix tabs --- source/mysql/commands.d | 26 ++--- source/mysql/connection.d | 120 ++++++++++----------- source/mysql/pool.d | 6 +- source/mysql/prepared.d | 59 +++++------ source/mysql/protocol/comms.d | 8 +- source/mysql/protocol/extra_types.d | 6 +- source/mysql/protocol/packet_helpers.d | 16 +-- source/mysql/protocol/packets.d | 24 ++--- source/mysql/protocol/sockets.d | 4 +- source/mysql/result.d | 64 +++++------ source/mysql/types.d | 141 +++++++++++++------------ 11 files changed, 235 insertions(+), 239 deletions(-) diff --git a/source/mysql/commands.d b/source/mysql/commands.d index 7dd5bce8..9ad1bfa2 100644 --- a/source/mysql/commands.d +++ b/source/mysql/commands.d @@ -1,4 +1,4 @@ -/++ +/++ Use a DB via plain SQL statements. Commands that are expected to return a result set - queries - have distinctive @@ -100,7 +100,7 @@ unittest chunkSize = 100; assert(cast(int)(totalSize / chunkSize) * chunkSize == totalSize); auto columnSpecial = ColumnSpecialization(0, 0xfc, chunkSize, &receiver); - + received = null; lastValueOfFinished = false; value = cn.queryValue(selectSQL, [columnSpecial]); @@ -110,7 +110,7 @@ unittest //assert(lastValueOfFinished == true); //assert(received == data); } - + // Use ColumnSpecialization with sql string, // and totalSize as a non-multiple of chunkSize { @@ -734,7 +734,7 @@ package Nullable!Variant queryValueImpl(ColumnSpecialization[] csa, Connection c { auto row = results.front; results.close(); - + if(row.length == 0) return Nullable!Variant(); else @@ -750,17 +750,17 @@ unittest import mysql.connection; import mysql.test.common; mixin(scopedCn); - + cn.exec("DROP TABLE IF EXISTS `execOverloads`"); cn.exec("CREATE TABLE `execOverloads` ( `i` INTEGER, `s` VARCHAR(50) ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - + immutable prepareSQL = "INSERT INTO `execOverloads` VALUES (?, ?)"; - + // Do the inserts, using exec - + // exec: const(char[]) sql assert(cn.exec("INSERT INTO `execOverloads` VALUES (1, \"aa\")") == 1); assert(cn.exec(prepareSQL, 2, "bb") == 1); @@ -774,18 +774,18 @@ unittest assert(cn.exec(prepared, 5, "ee") == 1); assert(prepared.getArg(0) == 5); assert(prepared.getArg(1) == "ee"); - + assert(cn.exec(prepared, [Variant(6), Variant("ff")]) == 1); assert(prepared.getArg(0) == 6); assert(prepared.getArg(1) == "ff"); - + // exec: bcPrepared sql auto bcPrepared = cn.prepareBackwardCompatImpl(prepareSQL); bcPrepared.setArgs(7, "gg"); assert(cn.exec(bcPrepared) == 1); assert(bcPrepared.getArg(0) == 7); assert(bcPrepared.getArg(1) == "gg"); - + // Check results auto rows = cn.query("SELECT * FROM `execOverloads`").array(); assert(rows.length == 7); @@ -822,7 +822,7 @@ unittest import mysql.connection; import mysql.test.common; mixin(scopedCn); - + cn.exec("DROP TABLE IF EXISTS `queryOverloads`"); cn.exec("CREATE TABLE `queryOverloads` ( `i` INTEGER, @@ -831,7 +831,7 @@ unittest cn.exec("INSERT INTO `queryOverloads` VALUES (1, \"aa\"), (2, \"bb\"), (3, \"cc\")"); immutable prepareSQL = "SELECT * FROM `queryOverloads` WHERE `i`=? AND `s`=?"; - + // Test query { Row[] rows; diff --git a/source/mysql/connection.d b/source/mysql/connection.d index bc5c57aa..fafe6125 100644 --- a/source/mysql/connection.d +++ b/source/mysql/connection.d @@ -1,4 +1,4 @@ -/// Connect to a MySQL/MariaDB server. +/// Connect to a MySQL/MariaDB server. module mysql.connection; import std.algorithm; @@ -208,7 +208,7 @@ package struct PreparedServerInfo ushort psWarnings; /// Number of parameters this statement takes. - /// + /// /// This will be the same on all connections, but it's returned /// by the server upon registration, so it's stored here. ushort numParams; @@ -218,7 +218,7 @@ package struct PreparedServerInfo /// This will be the same on all connections, but it's returned /// by the server upon registration, so it's stored here. PreparedStmtHeaders headers; - + /// Not actually from the server. Connection uses this to keep track /// of statements that should be treated as having been released. bool queuedForRelease = false; @@ -281,7 +281,7 @@ back to `prepare` again, and your upgrade will be complete. struct BackwardCompatPrepared { import std.variant; - + private Connection _conn; Prepared _prepared; @@ -292,7 +292,7 @@ struct BackwardCompatPrepared /++ This function is provided ONLY as a temporary aid in upgrading to mysql-native v2.0.0. - + See `BackwardCompatPrepared` for more info. +/ deprecated("Change 'preparedStmt.exec()' to 'conn.exec(preparedStmt)'") @@ -353,7 +353,7 @@ the connection when done). //TODO: All low-level commms should be moved into the mysql.protocol package. class Connection { - @safe: + @safe: /+ The Connection is responsible for handshaking with the server to establish authentication. It then passes client preferences to the server, and @@ -488,12 +488,12 @@ package: _cCaps = setClientFlags(_sCaps, clientCapabilities); this.authenticate(greeting); } - + /++ Forcefully close the socket without sending the quit command. - + Also resets internal state regardless of whether the connection is open or not. - + Needed in case an error leaves communatations in an undefined or non-recoverable state. +/ void kill() @@ -509,7 +509,7 @@ package: _lastCommandID++; // Invalidate result sets } - + /// Called whenever mysql-native needs to send a command to the server /// and be sure there aren't any pending results (which would prevent /// a new command from being sent). @@ -521,7 +521,7 @@ package: if(isAutoPurging) return; - + isAutoPurging = true; scope(exit) isAutoPurging = false; @@ -570,7 +570,7 @@ public: Construct opened connection. Throws `mysql.exceptions.MYX` upon failure to connect. - + If you are using Vibe.d, consider using `mysql.pool.MySQLPool` instead of creating a new Connection directly. That will provide certain benefits, such as reusing old connections and automatic cleanup (no need to close @@ -714,7 +714,7 @@ public: /++ Explicitly close the connection. - + Idiomatic use as follows is suggested: ------------------ { @@ -835,7 +835,7 @@ public: assert(!cn.closed); assert(range.front[0] == 1); } - + private void quit() in { @@ -864,7 +864,7 @@ public: $(LI [3]: db) $(LI [4]: port) ) - + (TODO: The connection string needs work to allow for semicolons in its parts!) +/ //TODO: Replace the return value with a proper struct. @@ -906,7 +906,7 @@ public: /++ Select a current database. - + Throws `mysql.exceptions.MYX` upon failure. Params: dbName = Name of the requested database @@ -920,7 +920,7 @@ public: /++ Check the server status. - + Throws `mysql.exceptions.MYX` upon failure. Returns: An `mysql.protocol.packets.OKErrorPacket` from which server status can be determined @@ -933,7 +933,7 @@ public: /++ Refresh some feature(s) of the server. - + Throws `mysql.exceptions.MYX` upon failure. Returns: An `mysql.protocol.packets.OKErrorPacket` from which server status can be determined @@ -946,12 +946,12 @@ public: /++ Flush any outstanding result set elements. - + When the server responds to a command that produces a result set, it queues the whole set of corresponding packets over the current connection. Before that `Connection` can embark on any new command, it must receive all of those packets and junk them. - + As of v1.1.4, this is done automatically as needed. But you can still call this manually to force a purge to occur when you want. @@ -964,7 +964,7 @@ public: /++ Get a textual report on the server status. - + (COM_STATISTICS) +/ string serverStats() @@ -974,11 +974,11 @@ public: /++ Enable multiple statement commands. - + This can be used later if this feature was not requested in the client capability flags. - + Warning: This functionality is currently untested. - + Params: on = Boolean value to turn the capability on or off. +/ //TODO: Need to test this @@ -1028,9 +1028,9 @@ public: /++ Manually register a prepared statement on this connection. - + Does nothing if statement is already registered on this connection. - + Calling this is not strictly necessary, as the prepared statement will automatically be registered upon its first use on any `Connection`. This is provided for those who prefer eager registration over lazy @@ -1049,10 +1049,10 @@ public: /++ Manually release a prepared statement on this connection. - + This method tells the server that it can dispose of the information it holds about the current prepared statement. - + Calling this is not strictly necessary. The server considers prepared statements to be per-connection, so they'll go away when the connection closes anyway. This is provided in case direct control is actually needed. @@ -1066,12 +1066,12 @@ public: their release at all, as it's not usually necessary. Or to periodically release all prepared statements, and simply allow mysql-native to automatically re-register them upon their next use. - + Notes: - + In actuality, the server might not immediately be told to release the statement (although `isRegistered` will still report `false`). - + This is because there could be a `mysql.result.ResultRange` with results still pending for retrieval, and the protocol doesn't allow sending commands (such as "release a prepared statement") to the server while data is pending. @@ -1080,7 +1080,7 @@ public: the next time a command (such as `mysql.commands.query` or `mysql.commands.exec`) is performed (because such commands automatically purge any pending results). - + This function does NOT auto-purge because, if this is ever called from automatic resource management cleanup (refcounting, RAII, etc), that would create ugly situations where hidden, implicit behavior triggers @@ -1090,7 +1090,7 @@ public: { release(prepared.sql); } - + ///ditto void release(const(char[]) sql) { @@ -1098,10 +1098,10 @@ public: // But need to be certain both situations are unittested. preparedRegistrations.queueForRelease(sql); } - + /++ Manually release all prepared statements on this connection. - + While minimal, every prepared statement registered on a connection does use up a small amount of resources in both mysql-native and on the server. Additionally, servers can be configured @@ -1111,22 +1111,22 @@ public: is quite high). Note also, that certain overloads of `mysql.commands.exec`, `mysql.commands.query`, etc. register prepared statements behind-the-scenes which are cached for quick re-use later. - + Therefore, it may occasionally be useful to clear out all prepared statements on a connection, together with all resources used by them (or at least leave the resources ready for garbage-collection). This function does just that. - + Note that this is ALWAYS COMPLETELY SAFE to call, even if you still have live prepared statements you intend to use again. This is safe because mysql-native will automatically register or re-register prepared statements as-needed. Notes: - + In actuality, the prepared statements might not be immediately released (although `isRegistered` will still report `false` for them). - + This is because there could be a `mysql.result.ResultRange` with results still pending for retrieval, and the protocol doesn't allow sending commands (such as "release a prepared statement") to the server while data is pending. @@ -1135,7 +1135,7 @@ public: the next time a command (such as `mysql.commands.query` or `mysql.commands.exec`) is performed (because such commands automatically purge any pending results). - + This function does NOT auto-purge because, if this is ever called from automatic resource management cleanup (refcounting, RAII, etc), that would create ugly situations where hidden, implicit behavior triggers @@ -1151,7 +1151,7 @@ public: unittest { mixin(scopedCn); - + cn.exec("DROP TABLE IF EXISTS `releaseAll`"); cn.exec("CREATE TABLE `releaseAll` (a INTEGER) ENGINE=InnoDB DEFAULT CHARSET=utf8"); @@ -1200,16 +1200,16 @@ unittest { import mysql.connection; import mysql.test.common; - + Prepared preparedInsert; Prepared preparedSelect; immutable insertSQL = "INSERT INTO `autoRegistration` VALUES (1), (2)"; immutable selectSQL = "SELECT `val` FROM `autoRegistration`"; int queryTupleResult; - + { mixin(scopedCn); - + // Setup cn.exec("DROP TABLE IF EXISTS `autoRegistration`"); cn.exec("CREATE TABLE `autoRegistration` ( @@ -1219,7 +1219,7 @@ unittest // Initial register preparedInsert = cn.prepare(insertSQL); preparedSelect = cn.prepare(selectSQL); - + // Test basic register, release, isRegistered assert(cn.isRegistered(preparedInsert)); assert(cn.isRegistered(preparedSelect)); @@ -1227,13 +1227,13 @@ unittest cn.release(preparedSelect); assert(!cn.isRegistered(preparedInsert)); assert(!cn.isRegistered(preparedSelect)); - + // Test manual re-register cn.register(preparedInsert); cn.register(preparedSelect); assert(cn.isRegistered(preparedInsert)); assert(cn.isRegistered(preparedSelect)); - + // Test double register cn.register(preparedInsert); cn.register(preparedSelect); @@ -1254,47 +1254,47 @@ unittest // Note that at this point, both prepared statements still exist, // but are no longer registered on any connection. In fact, there // are no open connections anymore. - + // Test auto-register: exec { mixin(scopedCn); - + assert(!cn.isRegistered(preparedInsert)); cn.exec(preparedInsert); assert(cn.isRegistered(preparedInsert)); } - + // Test auto-register: query { mixin(scopedCn); - + assert(!cn.isRegistered(preparedSelect)); cn.query(preparedSelect).each(); assert(cn.isRegistered(preparedSelect)); } - + // Test auto-register: queryRow { mixin(scopedCn); - + assert(!cn.isRegistered(preparedSelect)); cn.queryRow(preparedSelect); assert(cn.isRegistered(preparedSelect)); } - + // Test auto-register: queryRowTuple { mixin(scopedCn); - + assert(!cn.isRegistered(preparedSelect)); cn.queryRowTuple(preparedSelect, queryTupleResult); assert(cn.isRegistered(preparedSelect)); } - + // Test auto-register: queryValue { mixin(scopedCn); - + assert(!cn.isRegistered(preparedSelect)); cn.queryValue(preparedSelect); assert(cn.isRegistered(preparedSelect)); @@ -1309,14 +1309,14 @@ unittest { import mysql.escape; mixin(scopedCn); - + cn.exec("DROP TABLE IF EXISTS `issue81`"); cn.exec("CREATE TABLE `issue81` (a INTEGER) ENGINE=InnoDB DEFAULT CHARSET=utf8"); cn.exec("INSERT INTO `issue81` (a) VALUES (1)"); auto cn2 = new Connection(text("host=", cn._host, ";port=", cn._port, ";user=", cn._user, ";pwd=", cn._pwd)); scope(exit) cn2.close(); - + cn2.query("SELECT * FROM `"~mysqlEscape(cn._db).text~"`.`issue81`"); } @@ -1396,7 +1396,7 @@ debug(MYSQLN_TESTS) { import core.memory; mixin(scopedCn); - + cn.exec("DROP TABLE IF EXISTS `rcPrepared`"); cn.exec("CREATE TABLE `rcPrepared` ( `val` INTEGER diff --git a/source/mysql/pool.d b/source/mysql/pool.d index a96b9d39..a94761e9 100644 --- a/source/mysql/pool.d +++ b/source/mysql/pool.d @@ -99,7 +99,7 @@ version(IncludeMySQLPool) string m_database; ushort m_port; SvrCapFlags m_capFlags; - alias NewConnectionDelegate = void delegate(Connection) @safe; + alias NewConnectionDelegate = void delegate(Connection) @safe; NewConnectionDelegate m_onNewConnection; ConnectionPool!Connection m_pool; PreparedRegistrations!PreparedInfo preparedRegistrations; @@ -110,7 +110,7 @@ version(IncludeMySQLPool) } } - @safe: + @safe: /// Sets up a connection pool with the provided connection settings. /// @@ -238,7 +238,7 @@ version(IncludeMySQLPool) debug(MYSQLN_TESTS) unittest { - auto count = 0; + auto count = 0; void callback(Connection conn) { count++; diff --git a/source/mysql/prepared.d b/source/mysql/prepared.d index d8ec6ca4..4be31fe3 100644 --- a/source/mysql/prepared.d +++ b/source/mysql/prepared.d @@ -192,11 +192,6 @@ public: _psa.length = numParams; } - private void _assignArgImpl(T)(size_t index, auto ref T val) - { - _inParams[index] = val; - } - /++ Prepared statement parameter setter. @@ -231,7 +226,7 @@ public: enforce!MYX(index < _numParams, "Parameter index out of range."); - _assignArgImpl(index, val); + _inParams[index] = val; psn.pIndex = index; _psa[index] = psn; } @@ -245,13 +240,13 @@ public: setArg(index, val.get(), psn); } - deprecated("Using Variant is deprecated, please use MySQLVal instead") + deprecated("Using Variant is deprecated, please use MySQLVal instead") void setArg(T)(size_t index, T val, ParameterSpecialization psn = PSN(0, SQLType.INFER_FROM_D_TYPE, 0, null)) if(is(T == Variant)) { enforce!MYX(index < _numParams, "Parameter index out of range."); - _assignArgImpl(index, _toVal(val)); + _inParams[index] = _toVal(val); psn.pIndex = index; _psa[index] = psn; } @@ -295,8 +290,8 @@ public: // Note: Variant doesn't seem to support // `shared(T)` or `shared(const(T)`. Only `shared(immutable(T))`. - // Further note, shared immutable(int) is really - // immutable(int). This test is a duplicate, so removed. + // Further note, shared immutable(int) is really + // immutable(int). This test is a duplicate, so removed. // Test shared immutable(int) /*{ shared immutable(int) i = 113; @@ -365,19 +360,19 @@ public: } } - /// ditto - deprecated("Using Variant is deprecated, please use MySQLVal instead") + /// ditto + deprecated("Using Variant is deprecated, please use MySQLVal instead") void setArgs(Variant[] args, ParameterSpecialization[] psnList=null) - { - enforce!MYX(args.length == _numParams, "Param count supplied does not match prepared statement"); - foreach(i, ref arg; args) - setArg(i, arg); - if (psnList !is null) - { - foreach (PSN psn; psnList) - _psa[psn.pIndex] = psn; - } - } + { + enforce!MYX(args.length == _numParams, "Param count supplied does not match prepared statement"); + foreach(i, ref arg; args) + setArg(i, arg); + if (psnList !is null) + { + foreach (PSN psn; psnList) + _psa[psn.pIndex] = psn; + } + } /++ Prepared statement parameter getter. @@ -386,19 +381,19 @@ public: Params: index = The zero based index - Note: The Variant version of this function, getArg, is deprecated. - safeGetArg will eventually be renamed getArg when it is removed. + Note: The Variant version of this function, getArg, is deprecated. + safeGetArg will eventually be renamed getArg when it is removed. +/ - deprecated("Using Variant is deprecated, please use safeGetArg instead") + deprecated("Using Variant is deprecated, please use safeGetArg instead") Variant getArg(size_t index) { enforce!MYX(index < _numParams, "Parameter index out of range."); - // convert to Variant. - return _toVar(_inParams[index]); + // convert to Variant. + return _toVar(_inParams[index]); } - /// ditto + /// ditto MySQLVal safeGetArg(size_t index) @safe { enforce!MYX(index < _numParams, "Parameter index out of range."); @@ -556,7 +551,7 @@ to factor out common functionality needed by both `Connection` and `MySQLPool`. package struct PreparedRegistrations(Payload) if( isPreparedRegistrationsPayload!Payload) { - @safe: + @safe: /++ Lookup payload by sql string. @@ -608,9 +603,9 @@ package struct PreparedRegistrations(Payload) queueForRelease(sql); } - // Note: AA.clear does not invalidate any keys or values. In fact, it - // should really be safe/trusted, but is not. Therefore, we mark this - // as trusted. + // Note: AA.clear does not invalidate any keys or values. In fact, it + // should really be safe/trusted, but is not. Therefore, we mark this + // as trusted. /// Eliminate all records of both registered AND queued-for-release statements. void clear() @trusted { diff --git a/source/mysql/protocol/comms.d b/source/mysql/protocol/comms.d index c1305286..c13c0d8c 100644 --- a/source/mysql/protocol/comms.d +++ b/source/mysql/protocol/comms.d @@ -43,8 +43,8 @@ import mysql.protocol.sockets; import taggedalgebraic.taggedalgebraic; auto ref get(alias K, U)(auto ref TaggedAlgebraic!U ta) if (is(typeof(K) == TaggedAlgebraic!U.Kind)) { - import taggedalgebraic.taggedunion; - return (cast(TaggedUnion!U)ta).value!K; + import taggedalgebraic.taggedunion; + return (cast(TaggedUnion!U)ta).value!K; } @safe: @@ -52,7 +52,7 @@ auto ref get(alias K, U)(auto ref TaggedAlgebraic!U ta) if (is(typeof(K) == Tagg /// Low-level comms code relating to prepared statements. package struct ProtocolPrepared { - @safe: + @safe: import std.conv; import std.datetime; import mysql.types; @@ -134,7 +134,7 @@ package struct ProtocolPrepared with(MySQLVal.Kind) final switch (ts) { case BitRef: - isRef = true; goto case; + isRef = true; goto case; case Bit: if (ext == SQLType.INFER_FROM_D_TYPE) types[ct++] = SQLType.BIT; diff --git a/source/mysql/protocol/extra_types.d b/source/mysql/protocol/extra_types.d index be4c59d1..87dadc49 100644 --- a/source/mysql/protocol/extra_types.d +++ b/source/mysql/protocol/extra_types.d @@ -1,4 +1,4 @@ -/// Internal - Protocol-related data types. +/// Internal - Protocol-related data types. module mysql.protocol.extra_types; import std.exception; @@ -16,7 +16,7 @@ struct SQLValue bool isIncomplete; MySQLVal _value; - @safe: + @safe: // empty template as a template and non-template won't be added to the same overload set @property inout(MySQLVal) value()() inout { @@ -43,7 +43,7 @@ struct SQLValue /// Length Coded Binary Value struct LCB { - @safe: + @safe: /// True if the `LCB` contains a null value bool isNull; diff --git a/source/mysql/protocol/packet_helpers.d b/source/mysql/protocol/packet_helpers.d index f64a9d9b..9bb3f1f6 100644 --- a/source/mysql/protocol/packet_helpers.d +++ b/source/mysql/protocol/packet_helpers.d @@ -358,7 +358,7 @@ in } body { - auto result = packet.consume(N); + auto result = packet.consume(N); return (() @trusted => cast(string)result)(); } @@ -553,14 +553,14 @@ in } body { - union R { - T val = 0; - ubyte[T.sizeof] bytes; - } + union R { + T val = 0; + ubyte[T.sizeof] bytes; + } - R item; + R item; item.bytes[] = packet[0..T.sizeof]; - return item.val; + return item.val; } T consume(T, ubyte N=T.sizeof)(ref ubyte[] packet) pure nothrow @@ -635,7 +635,7 @@ SQLValue consumeNonBinaryValueIfComplete(T)(ref ubyte[] packet, bool unsigned) } else { - result.value = value.myto!T; + result.value = value.myto!T; } } } diff --git a/source/mysql/protocol/packets.d b/source/mysql/protocol/packets.d index e6544f4f..8e20c10f 100644 --- a/source/mysql/protocol/packets.d +++ b/source/mysql/protocol/packets.d @@ -1,4 +1,4 @@ -/// Internal - Tools for working with MySQL's communications packets. +/// Internal - Tools for working with MySQL's communications packets. module mysql.protocol.packets; import std.exception; @@ -29,7 +29,7 @@ See_Also: $(LINK http://forge.mysql.com/wiki/MySQL_Internals_ClientServer_Protoc +/ struct OKErrorPacket { - @safe: + @safe: bool error; ulong affected; ulong insertID; @@ -96,7 +96,7 @@ See_Also: $(LINK http://forge.mysql.com/wiki/MySQL_Internals_ClientServer_Protoc +/ struct FieldDescription { - @safe: + @safe: private: string _db; string _table; @@ -115,7 +115,7 @@ private: public: /++ Construct a `FieldDescription` from the raw data packet - + Params: packet = The packet contents excluding the 4 byte packet header +/ @@ -221,7 +221,7 @@ packets is sent, but they contain no useful information and are all the same. +/ struct ParamDescription { - @safe: + @safe: private: ushort _type; FieldFlags _flags; @@ -269,7 +269,7 @@ See_Also: $(LINK http://forge.mysql.com/wiki/MySQL_Internals_ClientServer_Protoc +/ struct EOFPacket { - @safe: + @safe: private: ushort _warnings; ushort _serverStatus; @@ -278,7 +278,7 @@ public: /++ Construct an `EOFPacket` struct from the raw data packet - + Params: packet = The packet contents excluding the 4 byte packet header +/ @@ -316,7 +316,7 @@ before the row data packets can be read. +/ struct ResultSetHeaders { - @safe: + @safe: import mysql.connection; private: @@ -329,7 +329,7 @@ public: /++ Construct a `ResultSetHeaders` struct from a sequence of `FieldDescription` packets and an EOF packet. - + Params: con = A `mysql.connection.Connection` via which the packets are read fieldCount = the number of fields/columns generated by the query @@ -358,7 +358,7 @@ public: /++ Add specialization information to one or more field descriptions. - + Currently, no specializations are implemented yet. Params: @@ -406,9 +406,9 @@ As noted in `ParamDescription` description, parameter descriptions are not fully +/ struct PreparedStmtHeaders { - @safe: + @safe: import mysql.connection; - + package: Connection _con; ushort _colCount, _paramCount; diff --git a/source/mysql/protocol/sockets.d b/source/mysql/protocol/sockets.d index dbfc28d1..a9b2468d 100644 --- a/source/mysql/protocol/sockets.d +++ b/source/mysql/protocol/sockets.d @@ -1,4 +1,4 @@ -/// Internal - Phobos and vibe.d sockets. +/// Internal - Phobos and vibe.d sockets. module mysql.protocol.sockets; import std.exception; @@ -108,7 +108,7 @@ version(Have_vibe_core) { /// Wraps a Vibe.d socket with the common interface class MySQLSocketVibeD : MySQLSocket { - @safe: + @safe: private PlainVibeDSocket socket; /// The socket should already be open diff --git a/source/mysql/result.d b/source/mysql/result.d index 3a15a35e..d1e1ab57 100644 --- a/source/mysql/result.d +++ b/source/mysql/result.d @@ -1,4 +1,4 @@ -/// Structures for data received: rows and result sets (ie, a range of rows). +/// Structures for data received: rows and result sets (ie, a range of rows). module mysql.result; import std.conv; @@ -39,17 +39,17 @@ private: string[] _names; public: - @safe: + @safe: /++ A constructor to extract the column data from a row data packet. - + If the data for the row exceeds the server's maximum packet size, then several packets will be sent for the row that taken together constitute a logical row data packet. The logic of the data recovery for a Row attempts to minimize the quantity of data that is bufferred. Users can assist in this by specifying chunked data transfer in cases where results sets can include long column values. - + Type_Mappings: $(TYPE_MAPPINGS) +/ this(Connection con, ref ubyte[] packet, ResultSetHeaders rh, bool binary) @@ -59,7 +59,7 @@ public: /++ Simplify retrieval of a column value by index. - + To check for null, use Variant's `type` property: `row[index].type == typeid(typeof(null))` @@ -110,7 +110,7 @@ public: /++ Check if a column in the result row was NULL - + Params: i = The zero based column index. +/ bool isNull(size_t i) const pure nothrow { return _nulls[i]; } @@ -125,13 +125,13 @@ public: /++ Move the content of the row into a compatible struct - + This method takes no account of NULL column values. If a column was NULL, the corresponding Variant value would be unchanged in those cases. - + The method will throw if the type of the Variant is not implicitly convertible to the corresponding struct member. - + Type_Mappings: $(TYPE_MAPPINGS) Params: @@ -172,19 +172,19 @@ public: { import std.stdio; - writefln("%(%s, %)", _values); + writefln("%(%s, %)", _values); } } /// ditto struct Row { - SafeRow safe; - alias safe this; - deprecated("Variant support is deprecated. Please use SafeRow instead of Row.") - Variant opIndex(size_t idx) const { - return _toVar(safe[idx]); - } + SafeRow safe; + alias safe this; + deprecated("Variant support is deprecated. Please use SafeRow instead of Row.") + Variant opIndex(size_t idx) const { + return _toVar(safe[idx]); + } } /++ @@ -332,7 +332,7 @@ public: /++ Get the number of rows retrieved so far. - + Note that this is not neccessarlly the same as the length of the range. +/ @property ulong rowCount() const pure nothrow { return _numRowsFetched; } @@ -340,19 +340,19 @@ public: struct ResultRange { - SafeResultRange safe; - alias safe this; - deprecated("Usage of Variant is deprecated. Use SafeResultRange instead of ResultRange") - inout(Row) front() inout { return inout(Row)(safe.front); } - - deprecated("Usage of Variant is deprecated. Use SafeResultRange instead of ResultRange") - Variant[string] asAA() - { - ensureValid(); - enforce!MYX(!safe.empty, "Attempted 'front' on exhausted result sequence."); - Variant[string] aa; - foreach (size_t i, string s; _colNames) - aa[s] = _toVar(_row._values[i]); - return aa; - } + SafeResultRange safe; + alias safe this; + deprecated("Usage of Variant is deprecated. Use SafeResultRange instead of ResultRange") + inout(Row) front() inout { return inout(Row)(safe.front); } + + deprecated("Usage of Variant is deprecated. Use SafeResultRange instead of ResultRange") + Variant[string] asAA() + { + ensureValid(); + enforce!MYX(!safe.empty, "Attempted 'front' on exhausted result sequence."); + Variant[string] aa; + foreach (size_t i, string s; _colNames) + aa[s] = _toVar(_row._values[i]); + return aa; + } } diff --git a/source/mysql/types.d b/source/mysql/types.d index 415543df..f685cb76 100644 --- a/source/mysql/types.d +++ b/source/mysql/types.d @@ -1,4 +1,4 @@ -/// Structures for MySQL types not built-in to D/Phobos. +/// Structures for MySQL types not built-in to D/Phobos. module mysql.types; import taggedalgebraic.taggedalgebraic; import std.datetime : DateTime, TimeOfDay, Date; @@ -34,49 +34,49 @@ struct Timestamp union _MYTYPE { - // blobs are const because of the indirection. In this case, it's not - // important because nobody is going to use MySQLVal to maintain their - // ubyte array. - const(ubyte)[] Blob; + // blobs are const because of the indirection. In this case, it's not + // important because nobody is going to use MySQLVal to maintain their + // ubyte array. + const(ubyte)[] Blob; @disableIndex: // do not want indexing on anything other than blobs. - typeof(null) Null; - bool Bit; - ubyte UByte; - byte Byte; - ushort UShort; - short Short; - uint UInt; - int Int; - ulong ULong; - long Long; - float Float; - double Double; - .DateTime DateTime; - TimeOfDay Time; - .Timestamp Timestamp; - .Date Date; + typeof(null) Null; + bool Bit; + ubyte UByte; + byte Byte; + ushort UShort; + short Short; + uint UInt; + int Int; + ulong ULong; + long Long; + float Float; + double Double; + .DateTime DateTime; + TimeOfDay Time; + .Timestamp Timestamp; + .Date Date; - string Text; + string Text; - // pointers - const(bool)* BitRef; - const(ubyte)* UByteRef; - const(byte)* ByteRef; - const(ushort)* UShortRef; - const(short)* ShortRef; - const(uint)* UIntRef; - const(int)* IntRef; - const(ulong)* ULongRef; - const(long)* LongRef; - const(float)* FloatRef; - const(double)* DoubleRef; - const(.DateTime)* DateTimeRef; - const(TimeOfDay)* TimeRef; - const(.Date)* DateRef; - const(string)* TextRef; - const(ubyte[])* BlobRef; - const(.Timestamp)* TimestampRef; + // pointers + const(bool)* BitRef; + const(ubyte)* UByteRef; + const(byte)* ByteRef; + const(ushort)* UShortRef; + const(short)* ShortRef; + const(uint)* UIntRef; + const(int)* IntRef; + const(ulong)* ULongRef; + const(long)* LongRef; + const(float)* FloatRef; + const(double)* DoubleRef; + const(.DateTime)* DateTimeRef; + const(TimeOfDay)* TimeRef; + const(.Date)* DateRef; + const(string)* TextRef; + const(ubyte[])* BlobRef; + const(.Timestamp)* TimestampRef; } alias MySQLVal = TaggedAlgebraic!_MYTYPE; @@ -85,46 +85,47 @@ alias MySQLVal = TaggedAlgebraic!_MYTYPE; import std.variant : Variant; package MySQLVal _toVal(Variant v) { - // unfortunately, we need to use a giant switch. But hopefully people will stop using Variant, and this will go away. - string ts = v.type.toString(); - bool isRef; - if (ts[$-1] == '*') - { - ts.length = ts.length-1; - isRef= true; - } + int x; + // unfortunately, we need to use a giant switch. But hopefully people will stop using Variant, and this will go away. + string ts = v.type.toString(); + bool isRef; + if (ts[$-1] == '*') + { + ts.length = ts.length-1; + isRef= true; + } - import std.meta; - import std.traits; - import mysql.exceptions; - alias AllTypes = AliasSeq!(bool, byte, ubyte, short, ushort, int, uint, long, ulong, float, double, DateTime, TimeOfDay, Date, string, ubyte[], Timestamp); - switch (ts) - { - static foreach(Type; AllTypes) - { - case fullyQualifiedName!Type: - case "const(" ~ fullyQualifiedName!Type ~ ")": - case "immutable(" ~ fullyQualifiedName!Type ~ ")": - case "shared(immutable(" ~ fullyQualifiedName!Type ~ "))": - if(isRef) - return MySQLVal(v.get!(const(Type*))); - else - return MySQLVal(v.get!(const(Type))); - } - default: - throw new MYX("Unsupported Database Variant Type: " ~ ts); - } + import std.meta; + import std.traits; + import mysql.exceptions; + alias AllTypes = AliasSeq!(bool, byte, ubyte, short, ushort, int, uint, long, ulong, float, double, DateTime, TimeOfDay, Date, string, ubyte[], Timestamp); + switch (ts) + { + static foreach(Type; AllTypes) + { + case fullyQualifiedName!Type: + case "const(" ~ fullyQualifiedName!Type ~ ")": + case "immutable(" ~ fullyQualifiedName!Type ~ ")": + case "shared(immutable(" ~ fullyQualifiedName!Type ~ "))": + if(isRef) + return MySQLVal(v.get!(const(Type*))); + else + return MySQLVal(v.get!(const(Type))); + } + default: + throw new MYX("Unsupported Database Variant Type: " ~ ts); + } } // convert MySQLVal to variant. Will eventually be removed when Variant support // is removed. package Variant _toVar(MySQLVal v) { - return v.apply!((a) => Variant(a)); + return v.apply!((a) => Variant(a)); } // helper to fix deficiency of convertsTo in TaggedAlgebraic package bool convertsTo(T)(ref MySQLVal val) { - return v.apply!((a) => is(typeof(a) : T)); + return v.apply!((a) => is(typeof(a) : T)); } From 4ebdcf33a440b0824e72cbf38f07963050cf14f3 Mon Sep 17 00:00:00 2001 From: Steven Schveighoffer Date: Thu, 28 Nov 2019 21:20:14 -0500 Subject: [PATCH 05/14] Add MySQLVal[] versions to all functions. Deprecate Variant[] versions. Switch queryValue return type to MySQLVal instead of Variant. --- source/mysql/commands.d | 87 +++++++++++++++++++++++++++++++++------ source/mysql/connection.d | 3 +- source/mysql/metadata.d | 35 ++++++++-------- source/mysql/result.d | 5 +-- source/mysql/types.d | 25 ++++++++++- 5 files changed, 121 insertions(+), 34 deletions(-) diff --git a/source/mysql/commands.d b/source/mysql/commands.d index 9ad1bfa2..cd5907b9 100644 --- a/source/mysql/commands.d +++ b/source/mysql/commands.d @@ -24,6 +24,7 @@ import mysql.protocol.constants; import mysql.protocol.extra_types; import mysql.protocol.packets; import mysql.result; +import mysql.types; /// This feature is not yet implemented. It currently has no effect. /+ @@ -205,12 +206,20 @@ ulong exec(T...)(Connection conn, const(char[]) sql, T args) return exec(conn, prepared); } ///ditto +deprecated("Variant support is deprecated. Use MySQLVal[] instead of Variant[]") ulong exec(Connection conn, const(char[]) sql, Variant[] args) { auto prepared = conn.prepare(sql); prepared.setArgs(args); return exec(conn, prepared); } +///ditto +ulong exec(Connection conn, const(char[]) sql, MySQLVal[] args) +{ + auto prepared = conn.prepare(sql); + prepared.setArgs(args); + return exec(conn, prepared); +} ///ditto ulong exec(Connection conn, ref Prepared prepared) @@ -228,12 +237,20 @@ ulong exec(T...)(Connection conn, ref Prepared prepared, T args) return exec(conn, prepared); } ///ditto +deprecated("Variant support is deprecated. Use MySQLVal[] instead of Variant[]") ulong exec(Connection conn, ref Prepared prepared, Variant[] args) { prepared.setArgs(args); return exec(conn, prepared); } +///ditto +ulong exec(Connection conn, ref Prepared prepared, MySQLVal[] args) +{ + prepared.setArgs(args); + return exec(conn, prepared); +} + ///ditto ulong exec(Connection conn, ref BackwardCompatPrepared prepared) { @@ -331,6 +348,7 @@ ResultRange query(T...)(Connection conn, const(char[]) sql, T args) return query(conn, prepared); } ///ditto +deprecated("Variant support is deprecated. Use MySQLVal[] instead of Variant[]") ResultRange query(Connection conn, const(char[]) sql, Variant[] args) { auto prepared = conn.prepare(sql); @@ -338,6 +356,14 @@ ResultRange query(Connection conn, const(char[]) sql, Variant[] args) return query(conn, prepared); } +///ditto +ResultRange query(Connection conn, const(char[]) sql, MySQLVal[] args) +{ + auto prepared = conn.prepare(sql); + prepared.setArgs(args); + return query(conn, prepared); +} + ///ditto ResultRange query(Connection conn, ref Prepared prepared) { @@ -354,11 +380,18 @@ ResultRange query(T...)(Connection conn, ref Prepared prepared, T args) return query(conn, prepared); } ///ditto +deprecated("Variant support is deprecated. Use MySQLVal[] instead of Variant[]") ResultRange query(Connection conn, ref Prepared prepared, Variant[] args) { prepared.setArgs(args); return query(conn, prepared); } +///ditto +ResultRange query(Connection conn, ref Prepared prepared, MySQLVal[] args) +{ + prepared.setArgs(args); + return query(conn, prepared); +} ///ditto ResultRange query(Connection conn, ref BackwardCompatPrepared prepared) @@ -461,6 +494,14 @@ Nullable!Row queryRow(T...)(Connection conn, const(char[]) sql, T args) return queryRow(conn, prepared); } ///ditto +Nullable!Row queryRow(Connection conn, const(char[]) sql, MySQLVal[] args) +{ + auto prepared = conn.prepare(sql); + prepared.setArgs(args); + return queryRow(conn, prepared); +} +///ditto +deprecated("Variant support is deprecated. Use MySQLVal[] instead of Variant[]") Nullable!Row queryRow(Connection conn, const(char[]) sql, Variant[] args) { auto prepared = conn.prepare(sql); @@ -484,11 +525,18 @@ Nullable!Row queryRow(T...)(Connection conn, ref Prepared prepared, T args) return queryRow(conn, prepared); } ///ditto +deprecated("Variant support is deprecated. Use MySQLVal[] instead of Variant[]") Nullable!Row queryRow(Connection conn, ref Prepared prepared, Variant[] args) { prepared.setArgs(args); return queryRow(conn, prepared); } +///ditto +Nullable!Row queryRow(Connection conn, ref Prepared prepared, MySQLVal[] args) +{ + prepared.setArgs(args); + return queryRow(conn, prepared); +} ///ditto Nullable!Row queryRow(Connection conn, ref BackwardCompatPrepared prepared) @@ -672,12 +720,12 @@ delegate. csa = An optional array of `ColumnSpecialization` structs. If you need to use this with a prepared statement, please use `mysql.prepared.Prepared.columnSpecials`. +/ -Nullable!Variant queryValue(Connection conn, const(char[]) sql, ColumnSpecialization[] csa = null) +Nullable!MySQLVal queryValue(Connection conn, const(char[]) sql, ColumnSpecialization[] csa = null) { return queryValueImpl(csa, conn, ExecQueryImplInfo(false, sql)); } ///ditto -Nullable!Variant queryValue(T...)(Connection conn, const(char[]) sql, T args) +Nullable!MySQLVal queryValue(T...)(Connection conn, const(char[]) sql, T args) if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) { auto prepared = conn.prepare(sql); @@ -685,7 +733,15 @@ Nullable!Variant queryValue(T...)(Connection conn, const(char[]) sql, T args) return queryValue(conn, prepared); } ///ditto -Nullable!Variant queryValue(Connection conn, const(char[]) sql, Variant[] args) +Nullable!MySQLVal queryValue(Connection conn, const(char[]) sql, MySQLVal[] args) +{ + auto prepared = conn.prepare(sql); + prepared.setArgs(args); + return queryValue(conn, prepared); +} +///ditto +deprecated("Variant support is deprecated. Use MySQLVal[] instead of Variant[]") +Nullable!MySQLVal queryValue(Connection conn, const(char[]) sql, Variant[] args) { auto prepared = conn.prepare(sql); prepared.setArgs(args); @@ -693,7 +749,7 @@ Nullable!Variant queryValue(Connection conn, const(char[]) sql, Variant[] args) } ///ditto -Nullable!Variant queryValue(Connection conn, ref Prepared prepared) +Nullable!MySQLVal queryValue(Connection conn, ref Prepared prepared) { auto preparedInfo = conn.registerIfNeeded(prepared.sql); auto result = queryValueImpl(prepared.columnSpecials, conn, prepared.getExecQueryImplInfo(preparedInfo.statementId)); @@ -701,21 +757,28 @@ Nullable!Variant queryValue(Connection conn, ref Prepared prepared) return result; } ///ditto -Nullable!Variant queryValue(T...)(Connection conn, ref Prepared prepared, T args) +Nullable!MySQLVal queryValue(T...)(Connection conn, ref Prepared prepared, T args) if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) { prepared.setArgs(args); return queryValue(conn, prepared); } ///ditto -Nullable!Variant queryValue(Connection conn, ref Prepared prepared, Variant[] args) +Nullable!MySQLVal queryValue(Connection conn, ref Prepared prepared, MySQLVal[] args) +{ + prepared.setArgs(args); + return queryValue(conn, prepared); +} +///ditto +deprecated("Variant support is deprecated. Use MySQLVal[] instead of Variant[]") +Nullable!MySQLVal queryValue(Connection conn, ref Prepared prepared, Variant[] args) { prepared.setArgs(args); return queryValue(conn, prepared); } ///ditto -Nullable!Variant queryValue(Connection conn, ref BackwardCompatPrepared prepared) +Nullable!MySQLVal queryValue(Connection conn, ref BackwardCompatPrepared prepared) { auto p = prepared.prepared; auto result = queryValue(conn, p); @@ -724,21 +787,21 @@ Nullable!Variant queryValue(Connection conn, ref BackwardCompatPrepared prepared } /// Common implementation for `queryValue` overloads. -package Nullable!Variant queryValueImpl(ColumnSpecialization[] csa, Connection conn, +package Nullable!MySQLVal queryValueImpl(ColumnSpecialization[] csa, Connection conn, ExecQueryImplInfo info) { auto results = queryImpl(csa, conn, info); if(results.empty) - return Nullable!Variant(); + return Nullable!MySQLVal(); else { - auto row = results.front; + auto row = results.safe.front; results.close(); if(row.length == 0) - return Nullable!Variant(); + return Nullable!MySQLVal(); else - return Nullable!Variant(row[0]); + return Nullable!MySQLVal(row[0]); } } diff --git a/source/mysql/connection.d b/source/mysql/connection.d index fafe6125..2fb749f0 100644 --- a/source/mysql/connection.d +++ b/source/mysql/connection.d @@ -17,6 +17,7 @@ import mysql.protocol.constants; import mysql.protocol.packets; import mysql.protocol.sockets; import mysql.result; +import mysql.types; debug(MYSQLN_TESTS) { import mysql.test.common; @@ -324,7 +325,7 @@ struct BackwardCompatPrepared ///ditto deprecated("Change 'preparedStmt.queryValue()' to 'conn.queryValue(preparedStmt)'") - Nullable!Variant queryValue() + Nullable!MySQLVal queryValue() { return .queryValue(_conn, _prepared); } diff --git a/source/mysql/metadata.d b/source/mysql/metadata.d index 217e57ae..ef058b85 100644 --- a/source/mysql/metadata.d +++ b/source/mysql/metadata.d @@ -1,4 +1,4 @@ -/// Retrieve metadata from a DB. +/// Retrieve metadata from a DB. module mysql.metadata; import std.array; @@ -10,6 +10,7 @@ import mysql.commands; import mysql.exceptions; import mysql.protocol.sockets; import mysql.result; +import mysql.types; /// A struct to hold column metadata struct ColumnInfo @@ -24,16 +25,16 @@ struct ColumnInfo size_t index; /++ Is the COLUMN_DEFAULT column (in the information schema's COLUMNS table) NULL? - + What this means: - + On MariaDB 10.2.7 and up: - Does the column have a default value? - + On MySQL and MariaDB 10.2.6 and below: - This can be true if the column doesn't have a default value OR if NULL is the column's default value. - + See_also: See COLUMN_DEFAULT description at $(LINK https://mariadb.com/kb/en/library/information-schema-columns-table/) @@ -41,7 +42,7 @@ struct ColumnInfo bool defaultNull; /++ The default value as a string if not NULL. - + Depending on the database (see comments for `defaultNull` and the related "see also" link there), this may be either `null` or `"NULL"` if the column's default value is NULL. @@ -100,7 +101,7 @@ information that is available to the connected user. This may well be quite limi struct MetaData { import mysql.connection; - + private: Connection _con; @@ -110,13 +111,13 @@ private: string query = procs ? "SHOW PROCEDURE STATUS WHERE db='": "SHOW FUNCTION STATUS WHERE db='"; query ~= _con.currentDB ~ "'"; - auto rs = _con.query(query).array; + auto rs = _con.query(query).safe.array; MySQLProcedure[] pa; pa.length = rs.length; foreach (size_t i; 0..rs.length) { MySQLProcedure foo; - Row r = rs[i]; + auto r = rs[i]; foreach (int j; 0..11) { if (r.isNull(j)) @@ -174,16 +175,16 @@ public: /++ List the available databases - + Note that if you have connected using the credentials of a user with limited permissions you may not get many results. - + Returns: An array of strings +/ string[] databases() { - auto rs = _con.query("SHOW DATABASES").array; + auto rs = _con.query("SHOW DATABASES").safe.array; string[] dbNames; dbNames.length = rs.length; foreach (size_t i; 0..rs.length) @@ -193,13 +194,13 @@ public: /++ List the tables in the current database - + Returns: An array of strings +/ string[] tables() { - auto rs = _con.query("SHOW TABLES").array; + auto rs = _con.query("SHOW TABLES").safe.array; string[] tblNames; tblNames.length = rs.length; foreach (size_t i; 0..rs.length) @@ -209,7 +210,7 @@ public: /++ Get column metadata for a table in the current database - + Params: table = The table name Returns: @@ -229,13 +230,13 @@ public: " COLUMN_KEY, EXTRA, PRIVILEGES, COLUMN_COMMENT" ~ " FROM information_schema.COLUMNS WHERE" ~ " table_schema='" ~ _con.currentDB ~ "' AND table_name='" ~ table ~ "'"; - auto rs = _con.query(query).array; + auto rs = _con.query(query).safe.array; ColumnInfo[] ca; ca.length = rs.length; foreach (size_t i; 0..rs.length) { ColumnInfo col; - Row r = rs[i]; + auto r = rs[i]; for (int j = 1; j < 19; j++) { string t; diff --git a/source/mysql/result.d b/source/mysql/result.d index d1e1ab57..4cdb379e 100644 --- a/source/mysql/result.d +++ b/source/mysql/result.d @@ -342,10 +342,9 @@ struct ResultRange { SafeResultRange safe; alias safe this; - deprecated("Usage of Variant is deprecated. Use SafeResultRange instead of ResultRange") - inout(Row) front() inout { return inout(Row)(safe.front); } + inout(Row) front() inout { return inout(Row)(safe.front); } - deprecated("Usage of Variant is deprecated. Use SafeResultRange instead of ResultRange") + deprecated("Usage of Variant is deprecated. Use safe member to get a SafeResultRange") Variant[string] asAA() { ensureValid(); diff --git a/source/mysql/types.d b/source/mysql/types.d index f685cb76..6c041047 100644 --- a/source/mysql/types.d +++ b/source/mysql/types.d @@ -2,6 +2,7 @@ module mysql.types; import taggedalgebraic.taggedalgebraic; import std.datetime : DateTime, TimeOfDay, Date; +public import taggedalgebraic.taggedalgebraic : get; /++ A simple struct to represent time difference. @@ -127,5 +128,27 @@ package Variant _toVar(MySQLVal v) // helper to fix deficiency of convertsTo in TaggedAlgebraic package bool convertsTo(T)(ref MySQLVal val) { - return v.apply!((a) => is(typeof(a) : T)); + return val.apply!((a) => is(typeof(a) : T)); +} + +package T coerce(T)(auto ref MySQLVal val) +{ + import std.conv : to; + static T convert(V)(ref V v) + { + static if(is(V : T)) + { + return v; + } + else static if(is(typeof(v.to!T()))) + { + return v.to!T; + } + else + { + import mysql.exceptions; + throw new MYX("Cannot coerce type " ~ V.stringof ~ " into type " ~ T.stringof); + } + } + return val.apply!convert(); } From 3b9e41e288f2aa9d68e0662bd286ce69c608a6f3 Mon Sep 17 00:00:00 2001 From: Steven Schveighoffer Date: Thu, 28 Nov 2019 21:58:52 -0500 Subject: [PATCH 06/14] Add Variant compatibility layer. Change name of Kind get function to kget. --- source/mysql/protocol/comms.d | 41 ++++++++++++++++++----------------- source/mysql/types.d | 28 ++++++++++++++++++++---- 2 files changed, 45 insertions(+), 24 deletions(-) diff --git a/source/mysql/protocol/comms.d b/source/mysql/protocol/comms.d index c13c0d8c..390c1fce 100644 --- a/source/mysql/protocol/comms.d +++ b/source/mysql/protocol/comms.d @@ -38,10 +38,11 @@ import mysql.protocol.packet_helpers; import mysql.protocol.packets; import mysql.protocol.sockets; -/** Gets the value stored in an algebraic type based on its data type. -*/ import taggedalgebraic.taggedalgebraic; -auto ref get(alias K, U)(auto ref TaggedAlgebraic!U ta) if (is(typeof(K) == TaggedAlgebraic!U.Kind)) + +// Trick tagged algebraic into getting the value based on the kind enum. Much +// easier than dealing with types when I already have the kind. +auto kget(alias K, U)(auto ref TaggedAlgebraic!U ta) if (is(typeof(K) == TaggedAlgebraic!U.Kind)) { import taggedalgebraic.taggedunion; return (cast(TaggedUnion!U)ta).value!K; @@ -142,7 +143,7 @@ package struct ProtocolPrepared types[ct++] = cast(ubyte) ext; types[ct++] = SIGNED; reAlloc(2); - bool bv = isRef? *v.get!BitRef : v.get!Bit; + bool bv = isRef? *v.kget!BitRef : v.kget!Bit; vals[vcl++] = 1; vals[vcl++] = bv? 0x31: 0x30; break; @@ -152,7 +153,7 @@ package struct ProtocolPrepared types[ct++] = SQLType.TINY; types[ct++] = SIGNED; reAlloc(1); - vals[vcl++] = isRef? *v.get!ByteRef : v.get!Byte; + vals[vcl++] = isRef? *v.kget!ByteRef : v.kget!Byte; break; case UByteRef: isRef = true; goto case; @@ -160,7 +161,7 @@ package struct ProtocolPrepared types[ct++] = SQLType.TINY; types[ct++] = UNSIGNED; reAlloc(1); - vals[vcl++] = isRef? *v.get!UByteRef : v.get!UByte; + vals[vcl++] = isRef? *v.kget!UByteRef : v.kget!UByte; break; case ShortRef: isRef = true; goto case; @@ -168,7 +169,7 @@ package struct ProtocolPrepared types[ct++] = SQLType.SHORT; types[ct++] = SIGNED; reAlloc(2); - short si = isRef? *v.get!ShortRef : v.get!Short; + short si = isRef? *v.kget!ShortRef : v.kget!Short; vals[vcl++] = cast(ubyte) (si & 0xff); vals[vcl++] = cast(ubyte) ((si >> 8) & 0xff); break; @@ -178,7 +179,7 @@ package struct ProtocolPrepared types[ct++] = SQLType.SHORT; types[ct++] = UNSIGNED; reAlloc(2); - ushort us = isRef? *v.get!UShortRef : v.get!UShort; + ushort us = isRef? *v.kget!UShortRef : v.kget!UShort; vals[vcl++] = cast(ubyte) (us & 0xff); vals[vcl++] = cast(ubyte) ((us >> 8) & 0xff); break; @@ -188,7 +189,7 @@ package struct ProtocolPrepared types[ct++] = SQLType.INT; types[ct++] = SIGNED; reAlloc(4); - int ii = isRef? *v.get!IntRef : v.get!Int; + int ii = isRef? *v.kget!IntRef : v.kget!Int; vals[vcl++] = cast(ubyte) (ii & 0xff); vals[vcl++] = cast(ubyte) ((ii >> 8) & 0xff); vals[vcl++] = cast(ubyte) ((ii >> 16) & 0xff); @@ -200,7 +201,7 @@ package struct ProtocolPrepared types[ct++] = SQLType.INT; types[ct++] = UNSIGNED; reAlloc(4); - uint ui = isRef? *v.get!UIntRef : v.get!UInt; + uint ui = isRef? *v.kget!UIntRef : v.kget!UInt; vals[vcl++] = cast(ubyte) (ui & 0xff); vals[vcl++] = cast(ubyte) ((ui >> 8) & 0xff); vals[vcl++] = cast(ubyte) ((ui >> 16) & 0xff); @@ -212,7 +213,7 @@ package struct ProtocolPrepared types[ct++] = SQLType.LONGLONG; types[ct++] = SIGNED; reAlloc(8); - long li = isRef? *v.get!LongRef : v.get!Long; + long li = isRef? *v.kget!LongRef : v.kget!Long; vals[vcl++] = cast(ubyte) (li & 0xff); vals[vcl++] = cast(ubyte) ((li >> 8) & 0xff); vals[vcl++] = cast(ubyte) ((li >> 16) & 0xff); @@ -228,7 +229,7 @@ package struct ProtocolPrepared types[ct++] = SQLType.LONGLONG; types[ct++] = UNSIGNED; reAlloc(8); - ulong ul = isRef? *v.get!ULongRef : v.get!ULong; + ulong ul = isRef? *v.kget!ULongRef : v.kget!ULong; vals[vcl++] = cast(ubyte) (ul & 0xff); vals[vcl++] = cast(ubyte) ((ul >> 8) & 0xff); vals[vcl++] = cast(ubyte) ((ul >> 16) & 0xff); @@ -244,7 +245,7 @@ package struct ProtocolPrepared types[ct++] = SQLType.FLOAT; types[ct++] = SIGNED; reAlloc(4); - float[1] f = [isRef? *v.get!FloatRef : v.get!Float]; + float[1] f = [isRef? *v.kget!FloatRef : v.kget!Float]; ubyte[] uba = cast(ubyte[]) f[]; vals[vcl .. vcl + uba.length] = uba[]; vcl += uba.length; @@ -255,7 +256,7 @@ package struct ProtocolPrepared types[ct++] = SQLType.DOUBLE; types[ct++] = SIGNED; reAlloc(8); - double[1] d = [isRef? *v.get!DoubleRef : v.get!Double]; + double[1] d = [isRef? *v.kget!DoubleRef : v.kget!Double]; ubyte[] uba = cast(ubyte[]) d[]; vals[vcl .. uba.length] = uba[]; vcl += uba.length; @@ -265,7 +266,7 @@ package struct ProtocolPrepared case Date: types[ct++] = SQLType.DATE; types[ct++] = SIGNED; - auto date = isRef? *v.get!DateRef : v.get!Date; + auto date = isRef? *v.kget!DateRef : v.kget!Date; ubyte[] da = pack(date); size_t l = da.length; reAlloc(l); @@ -277,7 +278,7 @@ package struct ProtocolPrepared case Time: types[ct++] = SQLType.TIME; types[ct++] = SIGNED; - auto time = isRef? *v.get!TimeRef : v.get!Time; + auto time = isRef? *v.kget!TimeRef : v.kget!Time; ubyte[] ta = pack(time); size_t l = ta.length; reAlloc(l); @@ -289,7 +290,7 @@ package struct ProtocolPrepared case DateTime: types[ct++] = SQLType.DATETIME; types[ct++] = SIGNED; - auto dt = isRef? *v.get!DateTimeRef : v.get!DateTime; + auto dt = isRef? *v.kget!DateTimeRef : v.kget!DateTime; ubyte[] da = pack(dt); size_t l = da.length; reAlloc(l); @@ -301,7 +302,7 @@ package struct ProtocolPrepared case Timestamp: types[ct++] = SQLType.TIMESTAMP; types[ct++] = SIGNED; - auto tms = isRef? *v.get!TimestampRef : v.get!Timestamp; + auto tms = isRef? *v.kget!TimestampRef : v.kget!Timestamp; auto dt = mysql.protocol.packet_helpers.toDateTime(tms.rep); ubyte[] da = pack(dt); size_t l = da.length; @@ -317,7 +318,7 @@ package struct ProtocolPrepared else types[ct++] = cast(ubyte) ext; types[ct++] = SIGNED; - const char[] ca = isRef? *v.get!TextRef : v.get!Text; + const char[] ca = isRef? *v.kget!TextRef : v.kget!Text; ubyte[] packed = packLCS(ca); reAlloc(packed.length); vals[vcl..vcl+packed.length] = packed[]; @@ -331,7 +332,7 @@ package struct ProtocolPrepared else types[ct++] = cast(ubyte) ext; types[ct++] = SIGNED; - const ubyte[] uba = isRef? *v.get!BlobRef : v.get!Blob; + const ubyte[] uba = isRef? *v.kget!BlobRef : v.kget!Blob; ubyte[] packed = packLCS(uba); reAlloc(packed.length); vals[vcl..vcl+packed.length] = packed[]; diff --git a/source/mysql/types.d b/source/mysql/types.d index 6c041047..43078d8e 100644 --- a/source/mysql/types.d +++ b/source/mysql/types.d @@ -2,7 +2,6 @@ module mysql.types; import taggedalgebraic.taggedalgebraic; import std.datetime : DateTime, TimeOfDay, Date; -public import taggedalgebraic.taggedalgebraic : get; /++ A simple struct to represent time difference. @@ -125,13 +124,34 @@ package Variant _toVar(MySQLVal v) return v.apply!((a) => Variant(a)); } -// helper to fix deficiency of convertsTo in TaggedAlgebraic -package bool convertsTo(T)(ref MySQLVal val) +/++ +Compatibility layer for std.variant.Variant. These functions provide methods +that TaggedAlgebraic does not provide in order to keep functionality that was +available with Variant. ++/ +bool convertsTo(T)(ref MySQLVal val) { return val.apply!((a) => is(typeof(a) : T)); } -package T coerce(T)(auto ref MySQLVal val) +/// ditto +T get(T)(auto ref MySQLVal val) +{ + static T convert(V)(ref V v) + { + static if(is(V : T)) + return v; + else + { + import mysql.exceptions; + throw new MYX("Cannot get type " ~ T.stringof ~ " with MySQLVal storing type " ~ V.stringof); + } + } + return val.apply!convert(); +} + +/// ditto +T coerce(T)(auto ref MySQLVal val) { import std.conv : to; static T convert(V)(ref V v) From 14858ca2af5de990584a04a59da090b4c6e84222 Mon Sep 17 00:00:00 2001 From: Steven Schveighoffer Date: Fri, 29 Nov 2019 03:13:58 -0500 Subject: [PATCH 07/14] More safe updates. Now builds the vibe tests. --- source/mysql/commands.d | 108 +++++++++++++++++--------------- source/mysql/connection.d | 3 + source/mysql/escape.d | 10 +-- source/mysql/metadata.d | 9 +-- source/mysql/prepared.d | 44 ++++++------- source/mysql/protocol/comms.d | 22 +++++-- source/mysql/result.d | 43 ++++++++----- source/mysql/test/common.d | 17 ++--- source/mysql/test/integration.d | 44 +++++++------ source/mysql/test/regression.d | 20 +++--- source/mysql/types.d | 43 ++++++++++--- 11 files changed, 215 insertions(+), 148 deletions(-) diff --git a/source/mysql/commands.d b/source/mysql/commands.d index cd5907b9..74e2f6fa 100644 --- a/source/mysql/commands.d +++ b/source/mysql/commands.d @@ -52,6 +52,8 @@ struct ColumnSpecialization ///ditto alias CSN = ColumnSpecialization; +@safe: + @("columnSpecial") debug(MYSQLN_TESTS) unittest @@ -70,7 +72,7 @@ unittest immutable totalSize = 1000; // Deliberately not a multiple of chunkSize below auto alph = cast(const(ubyte)[]) "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; auto data = alph.cycle.take(totalSize).array; - cn.exec("INSERT INTO `columnSpecial` VALUES (\""~(cast(string)data)~"\")"); + cn.exec("INSERT INTO `columnSpecial` VALUES (\""~(cast(const(char)[])data)~"\")"); // Common stuff int chunkSize; @@ -199,7 +201,7 @@ ulong exec(Connection conn, const(char[]) sql) } ///ditto ulong exec(T...)(Connection conn, const(char[]) sql, T args) - if(T.length > 0 && !is(T[0] == Variant[])) + if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[])) { auto prepared = conn.prepare(sql); prepared.setArgs(args); @@ -207,7 +209,7 @@ ulong exec(T...)(Connection conn, const(char[]) sql, T args) } ///ditto deprecated("Variant support is deprecated. Use MySQLVal[] instead of Variant[]") -ulong exec(Connection conn, const(char[]) sql, Variant[] args) +ulong exec(Connection conn, const(char[]) sql, Variant[] args) @system { auto prepared = conn.prepare(sql); prepared.setArgs(args); @@ -231,14 +233,14 @@ ulong exec(Connection conn, ref Prepared prepared) } ///ditto ulong exec(T...)(Connection conn, ref Prepared prepared, T args) - if(T.length > 0 && !is(T[0] == Variant[])) + if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[])) { prepared.setArgs(args); return exec(conn, prepared); } ///ditto deprecated("Variant support is deprecated. Use MySQLVal[] instead of Variant[]") -ulong exec(Connection conn, ref Prepared prepared, Variant[] args) +ulong exec(Connection conn, ref Prepared prepared, Variant[] args) @system { prepared.setArgs(args); return exec(conn, prepared); @@ -341,7 +343,7 @@ ResultRange query(Connection conn, const(char[]) sql, ColumnSpecialization[] csa } ///ditto ResultRange query(T...)(Connection conn, const(char[]) sql, T args) - if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) + if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) { auto prepared = conn.prepare(sql); prepared.setArgs(args); @@ -349,7 +351,7 @@ ResultRange query(T...)(Connection conn, const(char[]) sql, T args) } ///ditto deprecated("Variant support is deprecated. Use MySQLVal[] instead of Variant[]") -ResultRange query(Connection conn, const(char[]) sql, Variant[] args) +ResultRange query(Connection conn, const(char[]) sql, Variant[] args) @system { auto prepared = conn.prepare(sql); prepared.setArgs(args); @@ -374,14 +376,14 @@ ResultRange query(Connection conn, ref Prepared prepared) } ///ditto ResultRange query(T...)(Connection conn, ref Prepared prepared, T args) - if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) + if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) { prepared.setArgs(args); return query(conn, prepared); } ///ditto deprecated("Variant support is deprecated. Use MySQLVal[] instead of Variant[]") -ResultRange query(Connection conn, ref Prepared prepared, Variant[] args) +ResultRange query(Connection conn, ref Prepared prepared, Variant[] args) @system { prepared.setArgs(args); return query(conn, prepared); @@ -414,7 +416,7 @@ package ResultRange queryImpl(ColumnSpecialization[] csa, conn._rsh.addSpecializations(csa); conn._headersPending = false; - return ResultRange(SafeResultRange(conn, conn._rsh, conn._rsh.fieldNames)); + return ResultRange(conn, conn._rsh, conn._rsh.fieldNames); } /++ @@ -487,7 +489,7 @@ Nullable!Row queryRow(Connection conn, const(char[]) sql, ColumnSpecialization[] } ///ditto Nullable!Row queryRow(T...)(Connection conn, const(char[]) sql, T args) - if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) + if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) { auto prepared = conn.prepare(sql); prepared.setArgs(args); @@ -502,7 +504,7 @@ Nullable!Row queryRow(Connection conn, const(char[]) sql, MySQLVal[] args) } ///ditto deprecated("Variant support is deprecated. Use MySQLVal[] instead of Variant[]") -Nullable!Row queryRow(Connection conn, const(char[]) sql, Variant[] args) +Nullable!Row queryRow(Connection conn, const(char[]) sql, Variant[] args) @system { auto prepared = conn.prepare(sql); prepared.setArgs(args); @@ -519,14 +521,14 @@ Nullable!Row queryRow(Connection conn, ref Prepared prepared) } ///ditto Nullable!Row queryRow(T...)(Connection conn, ref Prepared prepared, T args) - if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) + if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) { prepared.setArgs(args); return queryRow(conn, prepared); } ///ditto deprecated("Variant support is deprecated. Use MySQLVal[] instead of Variant[]") -Nullable!Row queryRow(Connection conn, ref Prepared prepared, Variant[] args) +Nullable!Row queryRow(Connection conn, ref Prepared prepared, Variant[] args) @system { prepared.setArgs(args); return queryRow(conn, prepared); @@ -615,15 +617,17 @@ package void queryRowTupleImpl(T...)(Connection conn, ExecQueryImplInfo info, re ulong ra; enforce!MYXNoResultRecieved(execQueryImpl(conn, info, ra)); - Row rr = conn.getNextRow(); + auto rr = conn.getNextRow(); /+if (!rr._valid) // The result set was empty - not a crime. return;+/ enforce!MYX(rr._values.length == args.length, "Result column count does not match the target tuple."); foreach (size_t i, dummy; args) { - enforce!MYX(typeid(args[i]).toString() == rr._values[i].type.toString(), + import taggedalgebraic.taggedalgebraic : get, hasType; + enforce!MYX(rr._values[i].hasType!(T[i]), "Tuple "~to!string(i)~" type and column type are not compatible."); - args[i] = rr._values[i].get!(typeof(args[i])); + // use taggedalgebraic get to avoid extra calls. + args[i] = get!(T[i])(rr._values[i]); } // If there were more rows, flush them away // Question: Should I check in purgeResult and throw if there were - it's very inefficient to @@ -726,7 +730,7 @@ Nullable!MySQLVal queryValue(Connection conn, const(char[]) sql, ColumnSpecializ } ///ditto Nullable!MySQLVal queryValue(T...)(Connection conn, const(char[]) sql, T args) - if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) + if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) { auto prepared = conn.prepare(sql); prepared.setArgs(args); @@ -741,7 +745,7 @@ Nullable!MySQLVal queryValue(Connection conn, const(char[]) sql, MySQLVal[] args } ///ditto deprecated("Variant support is deprecated. Use MySQLVal[] instead of Variant[]") -Nullable!MySQLVal queryValue(Connection conn, const(char[]) sql, Variant[] args) +Nullable!MySQLVal queryValue(Connection conn, const(char[]) sql, Variant[] args) @system { auto prepared = conn.prepare(sql); prepared.setArgs(args); @@ -758,7 +762,7 @@ Nullable!MySQLVal queryValue(Connection conn, ref Prepared prepared) } ///ditto Nullable!MySQLVal queryValue(T...)(Connection conn, ref Prepared prepared, T args) - if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) + if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) { prepared.setArgs(args); return queryValue(conn, prepared); @@ -771,7 +775,7 @@ Nullable!MySQLVal queryValue(Connection conn, ref Prepared prepared, MySQLVal[] } ///ditto deprecated("Variant support is deprecated. Use MySQLVal[] instead of Variant[]") -Nullable!MySQLVal queryValue(Connection conn, ref Prepared prepared, Variant[] args) +Nullable!MySQLVal queryValue(Connection conn, ref Prepared prepared, Variant[] args) @system { prepared.setArgs(args); return queryValue(conn, prepared); @@ -795,7 +799,7 @@ package Nullable!MySQLVal queryValueImpl(ColumnSpecialization[] csa, Connection return Nullable!MySQLVal(); else { - auto row = results.safe.front; + auto row = results.front; results.close(); if(row.length == 0) @@ -827,7 +831,7 @@ unittest // exec: const(char[]) sql assert(cn.exec("INSERT INTO `execOverloads` VALUES (1, \"aa\")") == 1); assert(cn.exec(prepareSQL, 2, "bb") == 1); - assert(cn.exec(prepareSQL, [Variant(3), Variant("cc")]) == 1); + assert(cn.exec(prepareSQL, [MySQLVal(3), MySQLVal("cc")]) == 1); // exec: prepared sql auto prepared = cn.prepare(prepareSQL); @@ -838,7 +842,7 @@ unittest assert(prepared.getArg(0) == 5); assert(prepared.getArg(1) == "ee"); - assert(cn.exec(prepared, [Variant(6), Variant("ff")]) == 1); + assert(cn.exec(prepared, [MySQLVal(6), MySQLVal("ff")]) == 1); assert(prepared.getArg(0) == 6); assert(prepared.getArg(1) == "ff"); @@ -912,7 +916,7 @@ unittest assert(rows[0][0] == 2); assert(rows[0][1] == "bb"); - rows = cn.query(prepareSQL, [Variant(3), Variant("cc")]).array; + rows = cn.query(prepareSQL, [MySQLVal(3), MySQLVal("cc")]).array; assert(rows.length == 1); assert(rows[0].length == 2); assert(rows[0][0] == 3); @@ -933,7 +937,7 @@ unittest assert(rows[0][0] == 2); assert(rows[0][1] == "bb"); - rows = cn.query(prepared, [Variant(3), Variant("cc")]).array; + rows = cn.query(prepared, [MySQLVal(3), MySQLVal("cc")]).array; assert(rows.length == 1); assert(rows[0].length == 2); assert(rows[0][0] == 3); @@ -951,23 +955,25 @@ unittest // Test queryRow { - Nullable!Row row; + Nullable!Row nrow; + // avoid always saying nrow.get + Row row() { return nrow.get; } // String sql - row = cn.queryRow("SELECT * FROM `queryOverloads` WHERE `i`=1 AND `s`=\"aa\""); - assert(!row.isNull); + nrow = cn.queryRow("SELECT * FROM `queryOverloads` WHERE `i`=1 AND `s`=\"aa\""); + assert(!nrow.isNull); assert(row.length == 2); assert(row[0] == 1); assert(row[1] == "aa"); - row = cn.queryRow(prepareSQL, 2, "bb"); - assert(!row.isNull); + nrow = cn.queryRow(prepareSQL, 2, "bb"); + assert(!nrow.isNull); assert(row.length == 2); assert(row[0] == 2); assert(row[1] == "bb"); - row = cn.queryRow(prepareSQL, [Variant(3), Variant("cc")]); - assert(!row.isNull); + nrow = cn.queryRow(prepareSQL, [MySQLVal(3), MySQLVal("cc")]); + assert(!nrow.isNull); assert(row.length == 2); assert(row[0] == 3); assert(row[1] == "cc"); @@ -975,20 +981,20 @@ unittest // Prepared sql auto prepared = cn.prepare(prepareSQL); prepared.setArgs(1, "aa"); - row = cn.queryRow(prepared); - assert(!row.isNull); + nrow = cn.queryRow(prepared); + assert(!nrow.isNull); assert(row.length == 2); assert(row[0] == 1); assert(row[1] == "aa"); - row = cn.queryRow(prepared, 2, "bb"); - assert(!row.isNull); + nrow = cn.queryRow(prepared, 2, "bb"); + assert(!nrow.isNull); assert(row.length == 2); assert(row[0] == 2); assert(row[1] == "bb"); - row = cn.queryRow(prepared, [Variant(3), Variant("cc")]); - assert(!row.isNull); + nrow = cn.queryRow(prepared, [MySQLVal(3), MySQLVal("cc")]); + assert(!nrow.isNull); assert(row.length == 2); assert(row[0] == 3); assert(row[1] == "cc"); @@ -996,8 +1002,8 @@ unittest // BCPrepared sql auto bcPrepared = cn.prepareBackwardCompatImpl(prepareSQL); bcPrepared.setArgs(1, "aa"); - row = cn.queryRow(bcPrepared); - assert(!row.isNull); + nrow = cn.queryRow(bcPrepared); + assert(!nrow.isNull); assert(row.length == 2); assert(row[0] == 1); assert(row[1] == "aa"); @@ -1030,22 +1036,22 @@ unittest // Test queryValue { - Nullable!Variant value; + Nullable!MySQLVal value; // String sql value = cn.queryValue("SELECT * FROM `queryOverloads` WHERE `i`=1 AND `s`=\"aa\""); assert(!value.isNull); - assert(value.get.type != typeid(typeof(null))); + assert(value.get.kind != MySQLVal.Kind.Null); assert(value.get == 1); value = cn.queryValue(prepareSQL, 2, "bb"); assert(!value.isNull); - assert(value.get.type != typeid(typeof(null))); + assert(value.get.kind != MySQLVal.Kind.Null); assert(value.get == 2); - value = cn.queryValue(prepareSQL, [Variant(3), Variant("cc")]); + value = cn.queryValue(prepareSQL, [MySQLVal(3), MySQLVal("cc")]); assert(!value.isNull); - assert(value.get.type != typeid(typeof(null))); + assert(value.get.kind != MySQLVal.Kind.Null); assert(value.get == 3); // Prepared sql @@ -1053,17 +1059,17 @@ unittest prepared.setArgs(1, "aa"); value = cn.queryValue(prepared); assert(!value.isNull); - assert(value.get.type != typeid(typeof(null))); + assert(value.get.kind != MySQLVal.Kind.Null); assert(value.get == 1); value = cn.queryValue(prepared, 2, "bb"); assert(!value.isNull); - assert(value.get.type != typeid(typeof(null))); + assert(value.get.kind != MySQLVal.Kind.Null); assert(value.get == 2); - value = cn.queryValue(prepared, [Variant(3), Variant("cc")]); + value = cn.queryValue(prepared, [MySQLVal(3), MySQLVal("cc")]); assert(!value.isNull); - assert(value.get.type != typeid(typeof(null))); + assert(value.get.kind != MySQLVal.Kind.Null); assert(value.get == 3); // BCPrepared sql @@ -1071,7 +1077,7 @@ unittest bcPrepared.setArgs(1, "aa"); value = cn.queryValue(bcPrepared); assert(!value.isNull); - assert(value.get.type != typeid(typeof(null))); + assert(value.get.kind != MySQLVal.Kind.Null); assert(value.get == 1); } } diff --git a/source/mysql/connection.d b/source/mysql/connection.d index 2fb749f0..b7f3d9ff 100644 --- a/source/mysql/connection.d +++ b/source/mysql/connection.d @@ -18,6 +18,9 @@ import mysql.protocol.packets; import mysql.protocol.sockets; import mysql.result; import mysql.types; + +@safe: + debug(MYSQLN_TESTS) { import mysql.test.common; diff --git a/source/mysql/escape.d b/source/mysql/escape.d index 7654f5b4..6b58812d 100644 --- a/source/mysql/escape.d +++ b/source/mysql/escape.d @@ -45,15 +45,9 @@ struct MysqlEscape ( Input ) { Input input; - const void toString ( scope void delegate(const(char)[]) sink ) + const void toString ( scope void delegate(const(char)[]) @safe sink ) { - struct SinkOutputRange - { - void put ( const(char)[] t ) { sink(t); } - } - - SinkOutputRange r; - mysql_escape(input, r); + mysql_escape(input, sink); } } diff --git a/source/mysql/metadata.d b/source/mysql/metadata.d index ef058b85..f16a3ada 100644 --- a/source/mysql/metadata.d +++ b/source/mysql/metadata.d @@ -100,6 +100,7 @@ information that is available to the connected user. This may well be quite limi +/ struct MetaData { + @safe: import mysql.connection; private: @@ -111,7 +112,7 @@ private: string query = procs ? "SHOW PROCEDURE STATUS WHERE db='": "SHOW FUNCTION STATUS WHERE db='"; query ~= _con.currentDB ~ "'"; - auto rs = _con.query(query).safe.array; + auto rs = _con.query(query).array; MySQLProcedure[] pa; pa.length = rs.length; foreach (size_t i; 0..rs.length) @@ -184,7 +185,7 @@ public: +/ string[] databases() { - auto rs = _con.query("SHOW DATABASES").safe.array; + auto rs = _con.query("SHOW DATABASES").array; string[] dbNames; dbNames.length = rs.length; foreach (size_t i; 0..rs.length) @@ -200,7 +201,7 @@ public: +/ string[] tables() { - auto rs = _con.query("SHOW TABLES").safe.array; + auto rs = _con.query("SHOW TABLES").array; string[] tblNames; tblNames.length = rs.length; foreach (size_t i; 0..rs.length) @@ -230,7 +231,7 @@ public: " COLUMN_KEY, EXTRA, PRIVILEGES, COLUMN_COMMENT" ~ " FROM information_schema.COLUMNS WHERE" ~ " table_schema='" ~ _con.currentDB ~ "' AND table_name='" ~ table ~ "'"; - auto rs = _con.query(query).safe.array; + auto rs = _con.query(query).array; ColumnInfo[] ca; ca.length = rs.length; foreach (size_t i; 0..rs.length) diff --git a/source/mysql/prepared.d b/source/mysql/prepared.d index 4be31fe3..cbe496c1 100644 --- a/source/mysql/prepared.d +++ b/source/mysql/prepared.d @@ -149,6 +149,7 @@ INSERT/UPDATE/CREATE/etc, use `mysql.commands.exec`. +/ struct Prepared { + @safe: private: const(char)[] _sql; @@ -160,7 +161,7 @@ package: ColumnSpecialization[] _columnSpecials; ulong _lastInsertID; - ExecQueryImplInfo getExecQueryImplInfo(uint statementId) @safe + ExecQueryImplInfo getExecQueryImplInfo(uint statementId) { return ExecQueryImplInfo(true, null, statementId, _headers, _inParams, _psa); } @@ -183,7 +184,7 @@ public: including parameter descriptions, and result set field descriptions, followed by an EOF packet. +/ - this(const(char[]) sql, PreparedStmtHeaders headers, ushort numParams) @safe + this(const(char[]) sql, PreparedStmtHeaders headers, ushort numParams) { this._sql = sql; this._headers = headers; @@ -241,7 +242,7 @@ public: } deprecated("Using Variant is deprecated, please use MySQLVal instead") - void setArg(T)(size_t index, T val, ParameterSpecialization psn = PSN(0, SQLType.INFER_FROM_D_TYPE, 0, null)) + void setArg(T)(size_t index, T val, ParameterSpecialization psn = PSN(0, SQLType.INFER_FROM_D_TYPE, 0, null)) @system if(is(T == Variant)) { enforce!MYX(index < _numParams, "Parameter index out of range."); @@ -349,7 +350,7 @@ public: args = External list of MySQLVal to be used as parameters psnList = Any required specializations +/ - void setArgs(MySQLVal[] args, ParameterSpecialization[] psnList=null) @safe + void setArgs(MySQLVal[] args, ParameterSpecialization[] psnList=null) { enforce!MYX(args.length == _numParams, "Param count supplied does not match prepared statement"); _inParams[] = args[]; @@ -362,7 +363,7 @@ public: /// ditto deprecated("Using Variant is deprecated, please use MySQLVal instead") - void setArgs(Variant[] args, ParameterSpecialization[] psnList=null) + void setArgs(Variant[] args, ParameterSpecialization[] psnList=null) @system { enforce!MYX(args.length == _numParams, "Param count supplied does not match prepared statement"); foreach(i, ref arg; args) @@ -381,23 +382,22 @@ public: Params: index = The zero based index - Note: The Variant version of this function, getArg, is deprecated. - safeGetArg will eventually be renamed getArg when it is removed. + Note: The type of getArg's return is now MySQLVal. As a stop-gap measure, + mysql-native provides the vGetArg version. This version will be removed + in a future update. +/ - deprecated("Using Variant is deprecated, please use safeGetArg instead") - Variant getArg(size_t index) + MySQLVal getArg(size_t index) { enforce!MYX(index < _numParams, "Parameter index out of range."); - - // convert to Variant. - return _toVar(_inParams[index]); + return _inParams[index]; } /// ditto - MySQLVal safeGetArg(size_t index) @safe + deprecated("Using Variant is deprecated, please use getArg instead") + Variant vGetArg(size_t index) @system { - enforce!MYX(index < _numParams, "Parameter index out of range."); - return _inParams[index]; + // convert to Variant. + return getArg(index).asVariant; } /++ @@ -410,13 +410,13 @@ public: Params: index = The zero based index +/ - void setNullArg(size_t index) @safe + void setNullArg(size_t index) { setArg(index, null); } /// Gets the SQL command for this prepared statement. - const(char)[] sql() pure @safe const + const(char)[] sql() pure const { return _sql; } @@ -444,14 +444,14 @@ public: Nullable!int nullableInt; nullableInt.nullify(); preparedInsert.setArg(0, nullableInt); - assert(preparedInsert.getArg(0).type == typeid(typeof(null))); + assert(preparedInsert.getArg(0).kind == MySQLVal.Kind.Null); nullableInt = 7; preparedInsert.setArg(0, nullableInt); assert(preparedInsert.getArg(0) == 7); nullableInt.nullify(); preparedInsert.setArgs(nullableInt); - assert(preparedInsert.getArg(0).type == typeid(typeof(null))); + assert(preparedInsert.getArg(0).kind == MySQLVal.Kind.Null); nullableInt = 7; preparedInsert.setArgs(nullableInt); assert(preparedInsert.getArg(0) == 7); @@ -469,7 +469,7 @@ public: assert(rs.length == 2); assert(rs[0][0] == 5); assert(rs[1].isNull(0)); - assert(rs[1][0].type == typeid(typeof(null))); + assert(rs[1][0].kind == MySQLVal.Kind.Null); preparedInsert.setArg(0, MySQLVal(null)); cn.exec(preparedInsert); @@ -478,8 +478,8 @@ public: assert(rs[0][0] == 5); assert(rs[1].isNull(0)); assert(rs[2].isNull(0)); - assert(rs[1][0].type == typeid(typeof(null))); - assert(rs[2][0].type == typeid(typeof(null))); + assert(rs[1][0].kind == MySQLVal.Kind.Null); + assert(rs[2][0].kind == MySQLVal.Kind.Null); } /// Gets the number of arguments this prepared statement expects to be passed in. diff --git a/source/mysql/protocol/comms.d b/source/mysql/protocol/comms.d index 390c1fce..5d7203d7 100644 --- a/source/mysql/protocol/comms.d +++ b/source/mysql/protocol/comms.d @@ -324,6 +324,20 @@ package struct ProtocolPrepared vals[vcl..vcl+packed.length] = packed[]; vcl += packed.length; break; + case CTextRef: + isRef = true; goto case; + case CText: + if (ext == SQLType.INFER_FROM_D_TYPE) + types[ct++] = SQLType.VARCHAR; + else + types[ct++] = cast(ubyte) ext; + types[ct++] = SIGNED; + const char[] ca = isRef? *v.kget!CTextRef : v.kget!CText; + ubyte[] packed = packLCS(ca); + reAlloc(packed.length); + vals[vcl..vcl+packed.length] = packed[]; + vcl += packed.length; + break; case BlobRef: isRef = true; goto case; case Blob: @@ -794,7 +808,7 @@ package(mysql) ubyte[] makeToken(string password, ubyte[] authBuf) } /// Get the next `mysql.result.Row` of a pending result set. -package(mysql) SafeRow getNextRow(Connection conn) +package(mysql) Row getNextRow(Connection conn) { scope(failure) conn.kill(); @@ -804,7 +818,7 @@ package(mysql) SafeRow getNextRow(Connection conn) conn._headersPending = false; } ubyte[] packet; - SafeRow rr; + Row rr; packet = conn.getPacket(); if(packet.front == ResultPacketMarker.error) throw new MYXReceived(OKErrorPacket(packet), __FILE__, __LINE__); @@ -815,9 +829,9 @@ package(mysql) SafeRow getNextRow(Connection conn) return rr; } if (conn._binaryPending) - rr = SafeRow(conn, packet, conn._rsh, true); + rr = Row(conn, packet, conn._rsh, true); else - rr = SafeRow(conn, packet, conn._rsh, false); + rr = Row(conn, packet, conn._rsh, false); //rr._valid = true; return rr; } diff --git a/source/mysql/result.d b/source/mysql/result.d index 4cdb379e..90eaae48 100644 --- a/source/mysql/result.d +++ b/source/mysql/result.d @@ -12,7 +12,7 @@ import mysql.exceptions; import mysql.protocol.comms; import mysql.protocol.extra_types; import mysql.protocol.packets; -import mysql.types; +public import mysql.types; /++ A struct to represent a single row of a result set. @@ -28,7 +28,7 @@ I have been agitating for some kind of null indicator that can be set for a Variant without destroying its inherent type information. If this were the case, then the bool array could disappear. +/ -struct SafeRow +struct Row { import mysql.connection; @@ -177,13 +177,20 @@ public: } /// ditto -struct Row +deprecated("Usage of Variant is deprecated. Please switch code to use safe MySQLVal types") +UnsafeRow unsafe(Row r) { - SafeRow safe; + return UnsafeRow(r); +} + +/// ditto +struct UnsafeRow +{ + Row safe; alias safe this; - deprecated("Variant support is deprecated. Please use SafeRow instead of Row.") + deprecated("Variant support is deprecated. Please switch to using MySQLVal") Variant opIndex(size_t idx) const { - return _toVar(safe[idx]); + return safe[idx].asVariant; } } @@ -217,13 +224,13 @@ ResultRange oneAtATime = myConnection.query("SELECT * from myTable"); Row[] allAtOnce = myConnection.query("SELECT * from myTable").array; --- +/ -struct SafeResultRange +struct ResultRange { private: @safe: Connection _con; ResultSetHeaders _rsh; - SafeRow _row; // current row + Row _row; // current row string[] _colNames; size_t[string] _colNameIndicies; ulong _numRowsFetched; @@ -273,7 +280,7 @@ public: /++ Gets the current row +/ - @property inout(SafeRow) front() pure inout + @property inout(Row) front() pure inout { ensureValid(); enforce!MYX(!empty, "Attempted 'front' on exhausted result sequence."); @@ -338,20 +345,28 @@ public: @property ulong rowCount() const pure nothrow { return _numRowsFetched; } } -struct ResultRange +/// ditto +deprecated("Usage of Variant is deprecated. Please switch code to use safe MySQLVal types") +auto unsafe(ResultRange r) +{ + return UnsafeResultRange(r); +} + +/// ditto +struct UnsafeResultRange { - SafeResultRange safe; + ResultRange safe; alias safe this; - inout(Row) front() inout { return inout(Row)(safe.front); } + inout(UnsafeRow) front() inout { return inout(UnsafeRow)(safe.front); } - deprecated("Usage of Variant is deprecated. Use safe member to get a SafeResultRange") + deprecated("Variant support is deprecated. Please switch to using MySQLVal") Variant[string] asAA() { ensureValid(); enforce!MYX(!safe.empty, "Attempted 'front' on exhausted result sequence."); Variant[string] aa; foreach (size_t i, string s; _colNames) - aa[s] = _toVar(_row._values[i]); + aa[s] = _row._values[i].asVariant; return aa; } } diff --git a/source/mysql/test/common.d b/source/mysql/test/common.d index b4f330ee..51f5f694 100644 --- a/source/mysql/test/common.d +++ b/source/mysql/test/common.d @@ -1,4 +1,4 @@ -/++ +/++ Package mysql.test contains integration and regression tests, not unittests. Unittests (including regression unittests) are located together with the units they test. @@ -39,17 +39,18 @@ version(DoCoreTests) import std.conv; import std.datetime; + @safe: private @property string testConnectionStrFile() { import std.file, std.path; - + static string cached; if(!cached) cached = buildPath(thisExePath.dirName.dirName, "testConnectionStr.txt"); return cached; } - + @property string testConnectionStr() { import std.file, std.string; @@ -64,7 +65,7 @@ version(DoCoreTests) testConnectionStrFile, "host=localhost;port=3306;user=mysqln_test;pwd=pass123;db=mysqln_testdb" ); - + import std.stdio; writeln( "Connection string file for tests wasn't found, so a default "~ @@ -74,11 +75,11 @@ version(DoCoreTests) writeln(testConnectionStrFile); assert(false, "Halting so the user can check connection string settings."); } - - cached = cast(string) std.file.read(testConnectionStrFile); + + cached = std.file.readText(testConnectionStrFile); cached = cached.strip(); } - + return cached; } @@ -93,7 +94,7 @@ version(DoCoreTests) { auto result = cn.queryValue(query); assert(!result.isNull); - + // Timestamp is a bit special as it's converted to a DateTime when // returning from MySQL to avoid having to use a mysql specific type. static if(is(T == DateTime) && is(U == Timestamp)) diff --git a/source/mysql/test/integration.d b/source/mysql/test/integration.d index a4d60e5b..ab741ac3 100644 --- a/source/mysql/test/integration.d +++ b/source/mysql/test/integration.d @@ -1,4 +1,4 @@ -module mysql.test.integration; +module mysql.test.integration; import std.algorithm; import std.conv; @@ -23,6 +23,7 @@ import mysql.protocol.packets; import mysql.protocol.sockets; import mysql.result; import mysql.test.common; +@safe: alias indexOf = std.string.indexOf; // Needed on DMD 2.064.2 @@ -115,7 +116,7 @@ debug(MYSQLN_TESTS) unittest { import mysql.prepared; - + struct X { int a, b, c; @@ -246,7 +247,7 @@ unittest assert(rs[0][18] == "11234.4325", rs[0][18].toString()); stmt = cn.prepare("insert into basetest (intcol, stringcol) values(?, ?)"); - Variant[] va; + MySQLVal[] va; va.length = 2; va[0] = 42; va[1] = "The quick brown fox x"; @@ -259,7 +260,7 @@ unittest } stmt = cn.prepare("insert into basetest (intcol, stringcol) values(?, ?)"); - //Variant[] va; + //MySQLVal[] va; va.length = 2; va[0] = 42; va[1] = "The quick brown fox x"; @@ -279,7 +280,7 @@ unittest assert(a == 42 && b == "The quick brown fox"); stmt = cn.prepare("select intcol, stringcol from basetest where bytecol=? limit 1"); - Variant[] va2; + MySQLVal[] va2; va2.length = 1; va2[0] = cast(byte) -128; stmt.setArgs(va2); @@ -457,9 +458,9 @@ unittest count++; } assert(count == 2); - + initBaseTestTables(cn); - + string[] tList = md.tables(); count = 0; foreach (string t; tList) @@ -477,7 +478,7 @@ unittest See "COLUMN_DEFAULT" at: https://mariadb.com/kb/en/library/information-schema-columns-table/ +/ - + ColumnInfo[] ca = md.columns("basetest"); assert( ca[0].schema == schemaName && ca[0].table == "basetest" && ca[0].name == "boolcol" && ca[0].index == 0 && ca[0].nullable && ca[0].type == "bit" && ca[0].charsMax == -1 && ca[0].octetsMax == -1 && @@ -638,7 +639,7 @@ unittest /+ // Commented out because leaving args unspecified is currently unsupported, // and I'm not convinced it should be allowed. - + // Insert null - params defaults to null { cn.truncate("manytypes"); @@ -853,7 +854,7 @@ unittest import mysql.prepared; mixin(scopedCn); - void assertBasicTests(T, U)(string sqlType, U[] values ...) + void assertBasicTests(T, U)(string sqlType, U[] values ...) @safe { import std.array; immutable tablename = "`basic_"~sqlType.replace(" ", "")~"`"; @@ -877,13 +878,13 @@ unittest //assert(!cn.queryScalar(selectOneSql).hasValue); auto x = cn.queryValue(selectOneSql); assert(!x.isNull); - assert(x.get.type == typeid(typeof(null))); + assert(x.get.kind == MySQLVal.Kind.Null); // NULL as bound param auto inscmd = cn.prepare("INSERT INTO "~tablename~" VALUES (?)"); cn.exec("TRUNCATE "~tablename); - inscmd.setArgs([Variant(null)]); + inscmd.setArgs([MySQLVal(null)]); okp = cn.exec(inscmd); //assert(okp.affectedRows == 1, "value not inserted"); assert(okp == 1, "value not inserted"); @@ -891,7 +892,7 @@ unittest //assert(!cn.queryScalar(selectOneSql).hasValue); x = cn.queryValue(selectOneSql); assert(!x.isNull); - assert(x.type == typeid(typeof(null))); + assert(x.get.kind == MySQLVal.Kind.Null); // Values void assertBasicTestsValue(T, U)(U val) @@ -908,8 +909,11 @@ unittest { assertBasicTestsValue!(T)(value); assertBasicTestsValue!(T)(cast(const(U))value); - assertBasicTestsValue!(T)(cast(immutable(U))value); - assertBasicTestsValue!(T)(cast(shared(immutable(U)))value); + assertBasicTestsValue!(T)((() @trusted => cast(immutable(U))value)()); + // Note, shared(immutable(U)) is equivalent to immutable(U), so we + // are avoiding doing that test 2x. + static assert(is(shared(immutable(U)) == immutable(U))); + //assertBasicTestsValue!(T)(cast(shared(immutable(U)))value); } } @@ -949,8 +953,8 @@ unittest assertBasicTests!(ubyte[])("BLOB", "", "aoeu"); assertBasicTests!(ubyte[])("LONGBLOB", "", "aoeu"); - assertBasicTests!(ubyte[])("TINYBLOB", cast(byte[])"", cast(byte[])"aoeu"); - assertBasicTests!(ubyte[])("TINYBLOB", cast(char[])"", cast(char[])"aoeu"); + assertBasicTests!(ubyte[])("TINYBLOB", cast(ubyte[])"".dup, cast(ubyte[])"aoeu".dup); + assertBasicTests!(ubyte[])("TINYBLOB", "".dup, "aoeu".dup); assertBasicTests!Date("DATE", Date(2013, 10, 03)); assertBasicTests!DateTime("DATETIME", DateTime(2013, 10, 03, 12, 55, 35)); @@ -998,7 +1002,7 @@ unittest immutable selectNoRowsSQL = "SELECT * FROM `coupleTypes` WHERE s='no such match'"; auto prepared = cn.prepare(selectSQL); auto preparedSelectNoRows = cn.prepare(selectNoRowsSQL); - + { // Test query ResultRange rseq = cn.query(selectSQL); @@ -1099,7 +1103,7 @@ unittest } { - Nullable!Variant result; + Nullable!MySQLVal result; // Test queryValue result = cn.queryValue(selectSQL); @@ -1186,7 +1190,7 @@ unittest `name` VARCHAR(50) ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); } - + // Setup current working directory auto saveDir = getcwd(); scope(exit) diff --git a/source/mysql/test/regression.d b/source/mysql/test/regression.d index f41028b0..4ed49731 100644 --- a/source/mysql/test/regression.d +++ b/source/mysql/test/regression.d @@ -1,4 +1,4 @@ -/++ +/++ This contains regression tests for the issues at: https://github.com/rejectedsoftware/mysql-native/issues @@ -41,7 +41,7 @@ unittest `date` DATE ) ENGINE=InnoDB DEFAULT CHARSET=utf8" ); - + cn.exec("INSERT INTO `issue24` (`bit`, `date`) VALUES (1, '1970-01-01')"); cn.exec("INSERT INTO `issue24` (`bit`, `date`) VALUES (0, '1950-04-24')"); @@ -94,14 +94,14 @@ unittest `blob` BLOB ) ENGINE=InnoDB DEFAULT CHARSET=utf8" ); - + cn.exec("INSERT INTO `issue33` (`text`, `blob`) VALUES ('hello', 'world')"); auto stmt = cn.prepare("SELECT `text`, `blob` FROM `issue33`"); auto results = cn.query(stmt).array; assert(results.length == 1); auto pText = results[0][0].peek!string(); - auto pBlob = results[0][1].peek!(ubyte[])(); + auto pBlob = results[0][1].peek!(const(ubyte)[])(); assert(pText); assert(pBlob); assert(*pText == "hello"); @@ -164,7 +164,7 @@ unittest mixin(scopedCn); cn.exec("DROP TABLE IF EXISTS `issue56`"); cn.exec("CREATE TABLE `issue56` (a datetime DEFAULT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - + cn.exec("INSERT INTO `issue56` VALUES ('2015-03-28 00:00:00') ,('2015-03-29 00:00:00') @@ -233,11 +233,11 @@ debug(MYSQLN_TESTS) unittest { mixin(scopedCn); - + cn.exec("DROP TABLE IF EXISTS `issue133`"); cn.exec("CREATE TABLE `issue133` (a BIGINT UNSIGNED NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8"); cn.exec("INSERT INTO `issue133` (a) VALUES (NULL)"); - + auto prep = cn.prepare("SELECT a FROM `issue133`"); auto value = cn.queryValue(prep); @@ -262,7 +262,7 @@ unittest result.close(); } - + // Should not throw server packet out of order { ResultRange result; @@ -311,7 +311,7 @@ unittest assert(cn1 != cn2); } -// +// @("timestamp") debug(MYSQLN_TESTS) unittest @@ -321,7 +321,7 @@ unittest cn.exec("DROP TABLE IF EXISTS `issueX`"); cn.exec("CREATE TABLE `issueX` (a TIMESTAMP) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - + auto stmt = cn.prepare("INSERT INTO `issueX` (`a`) VALUES (?)"); stmt.setArgs(Timestamp(2011_11_11_12_20_02UL)); cn.exec(stmt); diff --git a/source/mysql/types.d b/source/mysql/types.d index 43078d8e..3136c548 100644 --- a/source/mysql/types.d +++ b/source/mysql/types.d @@ -2,6 +2,7 @@ module mysql.types; import taggedalgebraic.taggedalgebraic; import std.datetime : DateTime, TimeOfDay, Date; +import std.typecons : Nullable; /++ A simple struct to represent time difference. @@ -34,12 +35,12 @@ struct Timestamp union _MYTYPE { +@safeOnly: // blobs are const because of the indirection. In this case, it's not // important because nobody is going to use MySQLVal to maintain their // ubyte array. const(ubyte)[] Blob; -@disableIndex: // do not want indexing on anything other than blobs. typeof(null) Null; bool Bit; ubyte UByte; @@ -57,7 +58,8 @@ union _MYTYPE .Timestamp Timestamp; .Date Date; - string Text; + @disableIndex string Text; + @disableIndex const(char)[] CText; // pointers const(bool)* BitRef; @@ -75,6 +77,7 @@ union _MYTYPE const(TimeOfDay)* TimeRef; const(.Date)* DateRef; const(string)* TextRef; + const(char[])* CTextRef; const(ubyte[])* BlobRef; const(.Timestamp)* TimestampRef; } @@ -117,16 +120,26 @@ package MySQLVal _toVal(Variant v) } } -// convert MySQLVal to variant. Will eventually be removed when Variant support -// is removed. -package Variant _toVar(MySQLVal v) +/++ +Use this as a stop-gap measure in order to keep Variant compatibility. Append this to any function which returns a MySQLVal until you can update your code. ++/ +deprecated("Variant support is deprecated. Please switch to using MySQLVal") +Variant asVariant(MySQLVal v) { return v.apply!((a) => Variant(a)); } +deprecated("Variant support is deprecated. Please switch to using MySQLVal") +Nullable!Variant asVariant(Nullable!MySQLVal v) +{ + if(v.isNull) + return Nullable!Variant(); + return Nullable!Variant(v.get.asVariant); +} + /++ -Compatibility layer for std.variant.Variant. These functions provide methods -that TaggedAlgebraic does not provide in order to keep functionality that was +Compatibility layer for MySQLVal. These functions provide methods that +TaggedAlgebraic does not provide in order to keep functionality that was available with Variant. +/ bool convertsTo(T)(ref MySQLVal val) @@ -172,3 +185,19 @@ T coerce(T)(auto ref MySQLVal val) } return val.apply!convert(); } + +/// ditto +TypeInfo type(MySQLVal val) @safe pure nothrow +{ + return val.apply!((ref v) => typeid(v)); +} + +/// ditto +T *peek(T)(MySQLVal val) +{ + // use exact type. + import taggedalgebraic.taggedalgebraic : get; + if(val.hasType!T) + return &val.get!T; + return null; +} From 865266b4165a66d78e826d05a581b5ec3e786686 Mon Sep 17 00:00:00 2001 From: Steven Schveighoffer Date: Mon, 2 Dec 2019 10:58:40 -0500 Subject: [PATCH 08/14] Final fixups to get it to pass tests. --- dub.sdl | 2 +- dub.selections.json | 2 +- dub.selections.vibecore-1.0.0.json | 2 +- examples/homePage/dub.selections.json | 2 +- examples/homePage/dub.selections.vibecore-1.0.0.json | 2 +- examples/homePage/example.d | 6 +++--- source/mysql/protocol/comms.d | 3 ++- source/mysql/result.d | 4 ++-- source/mysql/test/regression.d | 2 +- source/mysql/types.d | 5 +++-- 10 files changed, 16 insertions(+), 14 deletions(-) diff --git a/dub.sdl b/dub.sdl index 9ffaf559..c410c8b9 100644 --- a/dub.sdl +++ b/dub.sdl @@ -5,7 +5,7 @@ copyright "Copyright (c) 2011-2019 Steve Teale, James W. Oliphant, Simen Endsj authors "Steve Teale" "James W. Oliphant" "Simen Endsjø" "Sönke Ludwig" "Sergey Shamov" "Nick Sabalausky" dependency "vibe-core" version="~>1.0" optional=true -dependency "taggedalgebraic" version="~>0.11.7" +dependency "taggedalgebraic" version="~>0.11.8" sourcePaths "source/" importPaths "source/" diff --git a/dub.selections.json b/dub.selections.json index 1a2f7fc7..b2dd43e4 100644 --- a/dub.selections.json +++ b/dub.selections.json @@ -8,7 +8,7 @@ "memutils": "0.4.13", "openssl": "1.1.4+1.0.1g", "stdx-allocator": "2.77.5", - "taggedalgebraic": "0.11.7", + "taggedalgebraic": "0.11.8", "unit-threaded": "0.7.55", "vibe-core": "1.7.0" } diff --git a/dub.selections.vibecore-1.0.0.json b/dub.selections.vibecore-1.0.0.json index 26606359..566f33e7 100644 --- a/dub.selections.vibecore-1.0.0.json +++ b/dub.selections.vibecore-1.0.0.json @@ -4,7 +4,7 @@ "eventcore": "0.8.48", "libasync": "0.8.4", "memutils": "0.4.13", - "taggedalgebraic": "0.11.6", + "taggedalgebraic": "0.11.8", "unit-threaded": "0.7.45", "vibe-core": "1.0.0" } diff --git a/examples/homePage/dub.selections.json b/examples/homePage/dub.selections.json index c849738f..fe39fbbb 100644 --- a/examples/homePage/dub.selections.json +++ b/examples/homePage/dub.selections.json @@ -8,7 +8,7 @@ "memutils": "0.4.13", "openssl": "1.1.4+1.0.1g", "stdx-allocator": "2.77.5", - "taggedalgebraic": "0.11.6", + "taggedalgebraic": "0.11.8", "unit-threaded": "0.7.45", "vibe-core": "1.7.0" } diff --git a/examples/homePage/dub.selections.vibecore-1.0.0.json b/examples/homePage/dub.selections.vibecore-1.0.0.json index 26606359..566f33e7 100644 --- a/examples/homePage/dub.selections.vibecore-1.0.0.json +++ b/examples/homePage/dub.selections.vibecore-1.0.0.json @@ -4,7 +4,7 @@ "eventcore": "0.8.48", "libasync": "0.8.4", "memutils": "0.4.13", - "taggedalgebraic": "0.11.6", + "taggedalgebraic": "0.11.8", "unit-threaded": "0.7.45", "vibe-core": "1.0.0" } diff --git a/examples/homePage/example.d b/examples/homePage/example.d index b992b28c..23f2ee7e 100644 --- a/examples/homePage/example.d +++ b/examples/homePage/example.d @@ -18,8 +18,8 @@ void main(string[] args) // Query ResultRange range = conn.query("SELECT * FROM `tablename`"); Row row = range.front; - Variant id = row[0]; - Variant name = row[1]; + auto id = row[0]; + auto name = row[1]; assert(id == 1); assert(name == "Ann"); @@ -32,7 +32,7 @@ void main(string[] args) "SELECT * FROM `tablename` WHERE `name`=? OR `name`=?", "Bob", "Bobby"); bobs.close(); // Skip them - + Row[] rs = conn.query( // Same SQL as above, but only prepared once and is reused! "SELECT * FROM `tablename` WHERE `name`=? OR `name`=?", "Bob", "Ann").array; // Get ALL the rows at once diff --git a/source/mysql/protocol/comms.d b/source/mysql/protocol/comms.d index 5d7203d7..3950aefd 100644 --- a/source/mysql/protocol/comms.d +++ b/source/mysql/protocol/comms.d @@ -341,12 +341,13 @@ package struct ProtocolPrepared case BlobRef: isRef = true; goto case; case Blob: + case CBlob: if (ext == SQLType.INFER_FROM_D_TYPE) types[ct++] = SQLType.TINYBLOB; else types[ct++] = cast(ubyte) ext; types[ct++] = SIGNED; - const ubyte[] uba = isRef? *v.kget!BlobRef : v.kget!Blob; + const ubyte[] uba = isRef? *v.kget!BlobRef : (ts == Blob ? v.kget!Blob : v.kget!CBlob); ubyte[] packed = packLCS(uba); reAlloc(packed.length); vals[vcl..vcl+packed.length] = packed[]; diff --git a/source/mysql/result.d b/source/mysql/result.d index 90eaae48..47620298 100644 --- a/source/mysql/result.d +++ b/source/mysql/result.d @@ -68,7 +68,7 @@ public: Params: i = the zero based index of the column whose value is required. Returns: A Variant holding the column value. +/ - inout(MySQLVal) opIndex(size_t i) inout + ref inout(MySQLVal) opIndex(size_t i) inout { enforce!MYX(_nulls.length > 0, format("Cannot get column index %d. There are no columns", i)); enforce!MYX(i < _nulls.length, format("Cannot get column index %d. The last available index is %d", i, _nulls.length-1)); @@ -189,7 +189,7 @@ struct UnsafeRow Row safe; alias safe this; deprecated("Variant support is deprecated. Please switch to using MySQLVal") - Variant opIndex(size_t idx) const { + Variant opIndex(size_t idx) { return safe[idx].asVariant; } } diff --git a/source/mysql/test/regression.d b/source/mysql/test/regression.d index 4ed49731..2bb5b6af 100644 --- a/source/mysql/test/regression.d +++ b/source/mysql/test/regression.d @@ -101,7 +101,7 @@ unittest auto results = cn.query(stmt).array; assert(results.length == 1); auto pText = results[0][0].peek!string(); - auto pBlob = results[0][1].peek!(const(ubyte)[])(); + auto pBlob = results[0][1].peek!(ubyte[])(); assert(pText); assert(pBlob); assert(*pText == "hello"); diff --git a/source/mysql/types.d b/source/mysql/types.d index 3136c548..95280185 100644 --- a/source/mysql/types.d +++ b/source/mysql/types.d @@ -39,7 +39,8 @@ union _MYTYPE // blobs are const because of the indirection. In this case, it's not // important because nobody is going to use MySQLVal to maintain their // ubyte array. - const(ubyte)[] Blob; + ubyte[] Blob; + const(ubyte)[] CBlob; typeof(null) Null; bool Bit; @@ -193,7 +194,7 @@ TypeInfo type(MySQLVal val) @safe pure nothrow } /// ditto -T *peek(T)(MySQLVal val) +T *peek(T)(ref MySQLVal val) { // use exact type. import taggedalgebraic.taggedalgebraic : get; From 04895444cb13062814fcbd12dd3011b8780cbcbe Mon Sep 17 00:00:00 2001 From: Steven Schveighoffer Date: Sun, 26 Jan 2020 00:27:40 -0500 Subject: [PATCH 09/14] Create a safe and unsafe command module, separating out the two systems. Add a MySQLSafeMode version to prepare for users that want to switch. This should be fully backwards compatible. --- source/mysql/commands.d | 1086 +------------------------------ source/mysql/connection.d | 10 +- source/mysql/metadata.d | 2 +- source/mysql/prepared.d | 7 +- source/mysql/protocol/comms.d | 8 +- source/mysql/result.d | 82 ++- source/mysql/safe/commands.d | 1004 ++++++++++++++++++++++++++++ source/mysql/test/common.d | 2 +- source/mysql/test/integration.d | 22 +- source/mysql/types.d | 18 +- source/mysql/unsafe/commands.d | 825 +++++++++++++++++++++++ 11 files changed, 1927 insertions(+), 1139 deletions(-) create mode 100644 source/mysql/safe/commands.d create mode 100644 source/mysql/unsafe/commands.d diff --git a/source/mysql/commands.d b/source/mysql/commands.d index 74e2f6fa..f8f456f4 100644 --- a/source/mysql/commands.d +++ b/source/mysql/commands.d @@ -1,1083 +1,5 @@ -/++ -Use a DB via plain SQL statements. - -Commands that are expected to return a result set - queries - have distinctive -methods that are enforced. That is it will be an error to call such a method -with an SQL command that does not produce a result set. So for commands like -SELECT, use the `query` functions. For other commands, like -INSERT/UPDATE/CREATE/etc, use `exec`. -+/ - module mysql.commands; - -import std.conv; -import std.exception; -import std.range; -import std.typecons; -import std.variant; - -import mysql.connection; -import mysql.exceptions; -import mysql.prepared; -import mysql.protocol.comms; -import mysql.protocol.constants; -import mysql.protocol.extra_types; -import mysql.protocol.packets; -import mysql.result; -import mysql.types; - -/// This feature is not yet implemented. It currently has no effect. -/+ -A struct to represent specializations of returned statement columns. - -If you are executing a query that will include result columns that are large objects, -it may be expedient to deal with the data as it is received rather than first buffering -it to some sort of byte array. These two variables allow for this. If both are provided -then the corresponding column will be fed to the stipulated delegate in chunks of -`chunkSize`, with the possible exception of the last chunk, which may be smaller. -The bool argument `finished` will be set to true when the last chunk is set. - -Be aware when specifying types for column specializations that for some reason the -field descriptions returned for a resultset have all of the types TINYTEXT, MEDIUMTEXT, -TEXT, LONGTEXT, TINYBLOB, MEDIUMBLOB, BLOB, and LONGBLOB lumped as type 0xfc -contrary to what it says in the protocol documentation. -+/ -struct ColumnSpecialization -{ - size_t cIndex; // parameter number 0 - number of params-1 - ushort type; - uint chunkSize; /// In bytes - void delegate(const(ubyte)[] chunk, bool finished) @safe chunkDelegate; -} -///ditto -alias CSN = ColumnSpecialization; - -@safe: - -@("columnSpecial") -debug(MYSQLN_TESTS) -unittest -{ - import std.array; - import std.range; - import mysql.test.common; - mixin(scopedCn); - - // Setup - cn.exec("DROP TABLE IF EXISTS `columnSpecial`"); - cn.exec("CREATE TABLE `columnSpecial` ( - `data` LONGBLOB - ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - - immutable totalSize = 1000; // Deliberately not a multiple of chunkSize below - auto alph = cast(const(ubyte)[]) "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - auto data = alph.cycle.take(totalSize).array; - cn.exec("INSERT INTO `columnSpecial` VALUES (\""~(cast(const(char)[])data)~"\")"); - - // Common stuff - int chunkSize; - immutable selectSQL = "SELECT `data` FROM `columnSpecial`"; - ubyte[] received; - bool lastValueOfFinished; - void receiver(const(ubyte)[] chunk, bool finished) @safe - { - assert(lastValueOfFinished == false); - - if(finished) - assert(chunk.length == chunkSize); - else - assert(chunk.length < chunkSize); // Not always true in general, but true in this unittest - - received ~= chunk; - lastValueOfFinished = finished; - } - - // Sanity check - auto value = cn.queryValue(selectSQL); - assert(!value.isNull); - assert(value.get == data); - - // Use ColumnSpecialization with sql string, - // and totalSize as a multiple of chunkSize - { - chunkSize = 100; - assert(cast(int)(totalSize / chunkSize) * chunkSize == totalSize); - auto columnSpecial = ColumnSpecialization(0, 0xfc, chunkSize, &receiver); - - received = null; - lastValueOfFinished = false; - value = cn.queryValue(selectSQL, [columnSpecial]); - assert(!value.isNull); - assert(value.get == data); - //TODO: ColumnSpecialization is not yet implemented - //assert(lastValueOfFinished == true); - //assert(received == data); - } - - // Use ColumnSpecialization with sql string, - // and totalSize as a non-multiple of chunkSize - { - chunkSize = 64; - assert(cast(int)(totalSize / chunkSize) * chunkSize != totalSize); - auto columnSpecial = ColumnSpecialization(0, 0xfc, chunkSize, &receiver); - - received = null; - lastValueOfFinished = false; - value = cn.queryValue(selectSQL, [columnSpecial]); - assert(!value.isNull); - assert(value.get == data); - //TODO: ColumnSpecialization is not yet implemented - //assert(lastValueOfFinished == true); - //assert(received == data); - } - - // Use ColumnSpecialization with prepared statement, - // and totalSize as a multiple of chunkSize - { - chunkSize = 100; - assert(cast(int)(totalSize / chunkSize) * chunkSize == totalSize); - auto columnSpecial = ColumnSpecialization(0, 0xfc, chunkSize, &receiver); - - received = null; - lastValueOfFinished = false; - auto prepared = cn.prepare(selectSQL); - prepared.columnSpecials = [columnSpecial]; - value = cn.queryValue(prepared); - assert(!value.isNull); - assert(value.get == data); - //TODO: ColumnSpecialization is not yet implemented - //assert(lastValueOfFinished == true); - //assert(received == data); - } -} - -/++ -Execute an SQL command or prepared statement, such as INSERT/UPDATE/CREATE/etc. - -This method is intended for commands such as which do not produce a result set -(otherwise, use one of the `query` functions instead.) If the SQL command does -produces a result set (such as SELECT), `mysql.exceptions.MYXResultRecieved` -will be thrown. - -If `args` is supplied, the sql string will automatically be used as a prepared -statement. Prepared statements are automatically cached by mysql-native, -so there's no performance penalty for using this multiple times for the -same statement instead of manually preparing a statement. - -If `args` and `prepared` are both provided, `args` will be used, -and any arguments that are already set in the prepared statement -will automatically be replaced with `args` (note, just like calling -`mysql.prepared.Prepared.setArgs`, this will also remove all -`mysql.prepared.ParameterSpecialization` that may have been applied). - -Only use the `const(char[]) sql` overload that doesn't take `args` -when you are not going to be using the same -command repeatedly and you are CERTAIN all the data you're sending is properly -escaped. Otherwise, consider using overload that takes a `Prepared`. - -If you need to use any `mysql.prepared.ParameterSpecialization`, use -`mysql.connection.prepare` to manually create a `mysql.prepared.Prepared`, -and set your parameter specializations using `mysql.prepared.Prepared.setArg` -or `mysql.prepared.Prepared.setArgs`. - -Type_Mappings: $(TYPE_MAPPINGS) - -Params: -conn = An open `mysql.connection.Connection` to the database. -sql = The SQL command to be run. -prepared = The prepared statement to be run. - -Returns: The number of rows affected. - -Example: ---- -auto myInt = 7; -auto rowsAffected = myConnection.exec("INSERT INTO `myTable` (`a`) VALUES (?)", myInt); ---- -+/ -ulong exec(Connection conn, const(char[]) sql) -{ - return execImpl(conn, ExecQueryImplInfo(false, sql)); -} -///ditto -ulong exec(T...)(Connection conn, const(char[]) sql, T args) - if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[])) -{ - auto prepared = conn.prepare(sql); - prepared.setArgs(args); - return exec(conn, prepared); -} -///ditto -deprecated("Variant support is deprecated. Use MySQLVal[] instead of Variant[]") -ulong exec(Connection conn, const(char[]) sql, Variant[] args) @system -{ - auto prepared = conn.prepare(sql); - prepared.setArgs(args); - return exec(conn, prepared); -} -///ditto -ulong exec(Connection conn, const(char[]) sql, MySQLVal[] args) -{ - auto prepared = conn.prepare(sql); - prepared.setArgs(args); - return exec(conn, prepared); -} - -///ditto -ulong exec(Connection conn, ref Prepared prepared) -{ - auto preparedInfo = conn.registerIfNeeded(prepared.sql); - auto ra = execImpl(conn, prepared.getExecQueryImplInfo(preparedInfo.statementId)); - prepared._lastInsertID = conn.lastInsertID; - return ra; -} -///ditto -ulong exec(T...)(Connection conn, ref Prepared prepared, T args) - if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[])) -{ - prepared.setArgs(args); - return exec(conn, prepared); -} -///ditto -deprecated("Variant support is deprecated. Use MySQLVal[] instead of Variant[]") -ulong exec(Connection conn, ref Prepared prepared, Variant[] args) @system -{ - prepared.setArgs(args); - return exec(conn, prepared); -} - -///ditto -ulong exec(Connection conn, ref Prepared prepared, MySQLVal[] args) -{ - prepared.setArgs(args); - return exec(conn, prepared); -} - -///ditto -ulong exec(Connection conn, ref BackwardCompatPrepared prepared) -{ - auto p = prepared.prepared; - auto result = exec(conn, p); - prepared._prepared = p; - return result; -} - -/// Common implementation for `exec` overloads -package ulong execImpl(Connection conn, ExecQueryImplInfo info) -{ - ulong rowsAffected; - bool receivedResultSet = execQueryImpl(conn, info, rowsAffected); - if(receivedResultSet) - { - conn.purgeResult(); - throw new MYXResultRecieved(); - } - - return rowsAffected; -} - -/++ -Execute an SQL SELECT command or prepared statement. - -This returns an input range of `mysql.result.Row`, so if you need random access -to the `mysql.result.Row` elements, simply call -$(LINK2 https://dlang.org/phobos/std_array.html#array, `std.array.array()`) -on the result. - -If the SQL command does not produce a result set (such as INSERT/CREATE/etc), -then `mysql.exceptions.MYXNoResultRecieved` will be thrown. Use -`exec` instead for such commands. - -If `args` is supplied, the sql string will automatically be used as a prepared -statement. Prepared statements are automatically cached by mysql-native, -so there's no performance penalty for using this multiple times for the -same statement instead of manually preparing a statement. - -If `args` and `prepared` are both provided, `args` will be used, -and any arguments that are already set in the prepared statement -will automatically be replaced with `args` (note, just like calling -`mysql.prepared.Prepared.setArgs`, this will also remove all -`mysql.prepared.ParameterSpecialization` that may have been applied). - -Only use the `const(char[]) sql` overload that doesn't take `args` -when you are not going to be using the same -command repeatedly and you are CERTAIN all the data you're sending is properly -escaped. Otherwise, consider using overload that takes a `Prepared`. - -If you need to use any `mysql.prepared.ParameterSpecialization`, use -`mysql.connection.prepare` to manually create a `mysql.prepared.Prepared`, -and set your parameter specializations using `mysql.prepared.Prepared.setArg` -or `mysql.prepared.Prepared.setArgs`. - -Type_Mappings: $(TYPE_MAPPINGS) - -Params: -conn = An open `mysql.connection.Connection` to the database. -sql = The SQL command to be run. -prepared = The prepared statement to be run. -csa = Not yet implemented. - -Returns: A (possibly empty) `mysql.result.ResultRange`. - -Example: ---- -ResultRange oneAtATime = myConnection.query("SELECT * from `myTable`"); -Row[] allAtOnce = myConnection.query("SELECT * from `myTable`").array; - -auto myInt = 7; -ResultRange rows = myConnection.query("SELECT * FROM `myTable` WHERE `a` = ?", myInt); ---- -+/ -/+ -Future text: -If there are long data items among the expected result columns you can use -the `csa` param to specify that they are to be subject to chunked transfer via a -delegate. - -csa = An optional array of `ColumnSpecialization` structs. If you need to -use this with a prepared statement, please use `mysql.prepared.Prepared.columnSpecials`. -+/ -ResultRange query(Connection conn, const(char[]) sql, ColumnSpecialization[] csa = null) -{ - return queryImpl(csa, conn, ExecQueryImplInfo(false, sql)); -} -///ditto -ResultRange query(T...)(Connection conn, const(char[]) sql, T args) - if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) -{ - auto prepared = conn.prepare(sql); - prepared.setArgs(args); - return query(conn, prepared); -} -///ditto -deprecated("Variant support is deprecated. Use MySQLVal[] instead of Variant[]") -ResultRange query(Connection conn, const(char[]) sql, Variant[] args) @system -{ - auto prepared = conn.prepare(sql); - prepared.setArgs(args); - return query(conn, prepared); -} - -///ditto -ResultRange query(Connection conn, const(char[]) sql, MySQLVal[] args) -{ - auto prepared = conn.prepare(sql); - prepared.setArgs(args); - return query(conn, prepared); -} - -///ditto -ResultRange query(Connection conn, ref Prepared prepared) -{ - auto preparedInfo = conn.registerIfNeeded(prepared.sql); - auto result = queryImpl(prepared.columnSpecials, conn, prepared.getExecQueryImplInfo(preparedInfo.statementId)); - prepared._lastInsertID = conn.lastInsertID; // Conceivably, this might be needed when multi-statements are enabled. - return result; -} -///ditto -ResultRange query(T...)(Connection conn, ref Prepared prepared, T args) - if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) -{ - prepared.setArgs(args); - return query(conn, prepared); -} -///ditto -deprecated("Variant support is deprecated. Use MySQLVal[] instead of Variant[]") -ResultRange query(Connection conn, ref Prepared prepared, Variant[] args) @system -{ - prepared.setArgs(args); - return query(conn, prepared); -} -///ditto -ResultRange query(Connection conn, ref Prepared prepared, MySQLVal[] args) -{ - prepared.setArgs(args); - return query(conn, prepared); -} - -///ditto -ResultRange query(Connection conn, ref BackwardCompatPrepared prepared) -{ - auto p = prepared.prepared; - auto result = query(conn, p); - prepared._prepared = p; - return result; -} - -/// Common implementation for `query` overloads -package ResultRange queryImpl(ColumnSpecialization[] csa, - Connection conn, ExecQueryImplInfo info) -{ - ulong ra; - enforce!MYXNoResultRecieved(execQueryImpl(conn, info, ra)); - - conn._rsh = ResultSetHeaders(conn, conn._fieldCount); - if(csa !is null) - conn._rsh.addSpecializations(csa); - - conn._headersPending = false; - return ResultRange(conn, conn._rsh, conn._rsh.fieldNames); -} - -/++ -Execute an SQL SELECT command or prepared statement where you only want the -first `mysql.result.Row`, if any. - -If the SQL command does not produce a result set (such as INSERT/CREATE/etc), -then `mysql.exceptions.MYXNoResultRecieved` will be thrown. Use -`exec` instead for such commands. - -If `args` is supplied, the sql string will automatically be used as a prepared -statement. Prepared statements are automatically cached by mysql-native, -so there's no performance penalty for using this multiple times for the -same statement instead of manually preparing a statement. - -If `args` and `prepared` are both provided, `args` will be used, -and any arguments that are already set in the prepared statement -will automatically be replaced with `args` (note, just like calling -`mysql.prepared.Prepared.setArgs`, this will also remove all -`mysql.prepared.ParameterSpecialization` that may have been applied). - -Only use the `const(char[]) sql` overload that doesn't take `args` -when you are not going to be using the same -command repeatedly and you are CERTAIN all the data you're sending is properly -escaped. Otherwise, consider using overload that takes a `Prepared`. - -If you need to use any `mysql.prepared.ParameterSpecialization`, use -`mysql.connection.prepare` to manually create a `mysql.prepared.Prepared`, -and set your parameter specializations using `mysql.prepared.Prepared.setArg` -or `mysql.prepared.Prepared.setArgs`. - -Type_Mappings: $(TYPE_MAPPINGS) - -Params: -conn = An open `mysql.connection.Connection` to the database. -sql = The SQL command to be run. -prepared = The prepared statement to be run. -csa = Not yet implemented. - -Returns: `Nullable!(mysql.result.Row)`: This will be null (check via `Nullable.isNull`) if the -query resulted in an empty result set. - -Example: ---- -auto myInt = 7; -Nullable!Row row = myConnection.queryRow("SELECT * FROM `myTable` WHERE `a` = ?", myInt); ---- -+/ -/+ -Future text: -If there are long data items among the expected result columns you can use -the `csa` param to specify that they are to be subject to chunked transfer via a -delegate. - -csa = An optional array of `ColumnSpecialization` structs. If you need to -use this with a prepared statement, please use `mysql.prepared.Prepared.columnSpecials`. -+/ -/+ -Future text: -If there are long data items among the expected result columns you can use -the `csa` param to specify that they are to be subject to chunked transfer via a -delegate. - -csa = An optional array of `ColumnSpecialization` structs. If you need to -use this with a prepared statement, please use `mysql.prepared.Prepared.columnSpecials`. -+/ -Nullable!Row queryRow(Connection conn, const(char[]) sql, ColumnSpecialization[] csa = null) -{ - return queryRowImpl(csa, conn, ExecQueryImplInfo(false, sql)); -} -///ditto -Nullable!Row queryRow(T...)(Connection conn, const(char[]) sql, T args) - if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) -{ - auto prepared = conn.prepare(sql); - prepared.setArgs(args); - return queryRow(conn, prepared); -} -///ditto -Nullable!Row queryRow(Connection conn, const(char[]) sql, MySQLVal[] args) -{ - auto prepared = conn.prepare(sql); - prepared.setArgs(args); - return queryRow(conn, prepared); -} -///ditto -deprecated("Variant support is deprecated. Use MySQLVal[] instead of Variant[]") -Nullable!Row queryRow(Connection conn, const(char[]) sql, Variant[] args) @system -{ - auto prepared = conn.prepare(sql); - prepared.setArgs(args); - return queryRow(conn, prepared); -} - -///ditto -Nullable!Row queryRow(Connection conn, ref Prepared prepared) -{ - auto preparedInfo = conn.registerIfNeeded(prepared.sql); - auto result = queryRowImpl(prepared.columnSpecials, conn, prepared.getExecQueryImplInfo(preparedInfo.statementId)); - prepared._lastInsertID = conn.lastInsertID; // Conceivably, this might be needed when multi-statements are enabled. - return result; -} -///ditto -Nullable!Row queryRow(T...)(Connection conn, ref Prepared prepared, T args) - if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) -{ - prepared.setArgs(args); - return queryRow(conn, prepared); -} -///ditto -deprecated("Variant support is deprecated. Use MySQLVal[] instead of Variant[]") -Nullable!Row queryRow(Connection conn, ref Prepared prepared, Variant[] args) @system -{ - prepared.setArgs(args); - return queryRow(conn, prepared); -} -///ditto -Nullable!Row queryRow(Connection conn, ref Prepared prepared, MySQLVal[] args) -{ - prepared.setArgs(args); - return queryRow(conn, prepared); -} - -///ditto -Nullable!Row queryRow(Connection conn, ref BackwardCompatPrepared prepared) -{ - auto p = prepared.prepared; - auto result = queryRow(conn, p); - prepared._prepared = p; - return result; -} - -/// Common implementation for `querySet` overloads. -package Nullable!Row queryRowImpl(ColumnSpecialization[] csa, Connection conn, - ExecQueryImplInfo info) -{ - auto results = queryImpl(csa, conn, info); - if(results.empty) - return Nullable!Row(); - else - { - auto row = results.front; - results.close(); - return Nullable!Row(row); - } -} - -/++ -Execute an SQL SELECT command or prepared statement where you only want the -first `mysql.result.Row`, and place result values into a set of D variables. - -This method will throw if any column type is incompatible with the corresponding D variable. - -Unlike the other query functions, queryRowTuple will throw -`mysql.exceptions.MYX` if the result set is empty -(and thus the reference variables passed in cannot be filled). - -If the SQL command does not produce a result set (such as INSERT/CREATE/etc), -then `mysql.exceptions.MYXNoResultRecieved` will be thrown. Use -`exec` instead for such commands. - -Only use the `const(char[]) sql` overload when you are not going to be using the same -command repeatedly and you are CERTAIN all the data you're sending is properly -escaped. Otherwise, consider using overload that takes a `Prepared`. - -Type_Mappings: $(TYPE_MAPPINGS) - -Params: -conn = An open `mysql.connection.Connection` to the database. -sql = The SQL command to be run. -prepared = The prepared statement to be run. -args = The variables, taken by reference, to receive the values. -+/ -void queryRowTuple(T...)(Connection conn, const(char[]) sql, ref T args) -{ - return queryRowTupleImpl(conn, ExecQueryImplInfo(false, sql), args); -} - -///ditto -void queryRowTuple(T...)(Connection conn, ref Prepared prepared, ref T args) -{ - auto preparedInfo = conn.registerIfNeeded(prepared.sql); - queryRowTupleImpl(conn, prepared.getExecQueryImplInfo(preparedInfo.statementId), args); - prepared._lastInsertID = conn.lastInsertID; // Conceivably, this might be needed when multi-statements are enabled. -} - -///ditto -void queryRowTuple(T...)(Connection conn, ref BackwardCompatPrepared prepared, ref T args) -{ - auto p = prepared.prepared; - queryRowTuple(conn, p, args); - prepared._prepared = p; -} - -/// Common implementation for `queryRowTuple` overloads. -package void queryRowTupleImpl(T...)(Connection conn, ExecQueryImplInfo info, ref T args) -{ - ulong ra; - enforce!MYXNoResultRecieved(execQueryImpl(conn, info, ra)); - - auto rr = conn.getNextRow(); - /+if (!rr._valid) // The result set was empty - not a crime. - return;+/ - enforce!MYX(rr._values.length == args.length, "Result column count does not match the target tuple."); - foreach (size_t i, dummy; args) - { - import taggedalgebraic.taggedalgebraic : get, hasType; - enforce!MYX(rr._values[i].hasType!(T[i]), - "Tuple "~to!string(i)~" type and column type are not compatible."); - // use taggedalgebraic get to avoid extra calls. - args[i] = get!(T[i])(rr._values[i]); - } - // If there were more rows, flush them away - // Question: Should I check in purgeResult and throw if there were - it's very inefficient to - // allow sloppy SQL that does not ensure just one row! - conn.purgeResult(); -} - -// Test what happends when queryRowTuple receives no rows -@("queryRowTuple_noRows") -debug(MYSQLN_TESTS) -unittest -{ - import mysql.test.common : scopedCn, createCn; - mixin(scopedCn); - - cn.exec("DROP TABLE IF EXISTS `queryRowTuple_noRows`"); - cn.exec("CREATE TABLE `queryRowTuple_noRows` ( - `val` INTEGER - ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - - immutable selectSQL = "SELECT * FROM `queryRowTuple_noRows`"; - int queryTupleResult; - assertThrown!MYX(cn.queryRowTuple(selectSQL, queryTupleResult)); -} - -/++ -Execute an SQL SELECT command or prepared statement and return a single value: -the first column of the first row received. - -If the query did not produce any rows, or the rows it produced have zero columns, -this will return `Nullable!Variant()`, ie, null. Test for this with `result.isNull`. - -If the query DID produce a result, but the value actually received is NULL, -then `result.isNull` will be FALSE, and `result.get` will produce a Variant -which CONTAINS null. Check for this with `result.get.type == typeid(typeof(null))`. - -If the SQL command does not produce a result set (such as INSERT/CREATE/etc), -then `mysql.exceptions.MYXNoResultRecieved` will be thrown. Use -`exec` instead for such commands. - -If `args` is supplied, the sql string will automatically be used as a prepared -statement. Prepared statements are automatically cached by mysql-native, -so there's no performance penalty for using this multiple times for the -same statement instead of manually preparing a statement. - -If `args` and `prepared` are both provided, `args` will be used, -and any arguments that are already set in the prepared statement -will automatically be replaced with `args` (note, just like calling -`mysql.prepared.Prepared.setArgs`, this will also remove all -`mysql.prepared.ParameterSpecialization` that may have been applied). - -Only use the `const(char[]) sql` overload that doesn't take `args` -when you are not going to be using the same -command repeatedly and you are CERTAIN all the data you're sending is properly -escaped. Otherwise, consider using overload that takes a `Prepared`. - -If you need to use any `mysql.prepared.ParameterSpecialization`, use -`mysql.connection.prepare` to manually create a `mysql.prepared.Prepared`, -and set your parameter specializations using `mysql.prepared.Prepared.setArg` -or `mysql.prepared.Prepared.setArgs`. - -Type_Mappings: $(TYPE_MAPPINGS) - -Params: -conn = An open `mysql.connection.Connection` to the database. -sql = The SQL command to be run. -prepared = The prepared statement to be run. -csa = Not yet implemented. - -Returns: `Nullable!Variant`: This will be null (check via `Nullable.isNull`) if the -query resulted in an empty result set. - -Example: ---- -auto myInt = 7; -Nullable!Variant value = myConnection.queryRow("SELECT * FROM `myTable` WHERE `a` = ?", myInt); ---- -+/ -/+ -Future text: -If there are long data items among the expected result columns you can use -the `csa` param to specify that they are to be subject to chunked transfer via a -delegate. - -csa = An optional array of `ColumnSpecialization` structs. If you need to -use this with a prepared statement, please use `mysql.prepared.Prepared.columnSpecials`. -+/ -/+ -Future text: -If there are long data items among the expected result columns you can use -the `csa` param to specify that they are to be subject to chunked transfer via a -delegate. - -csa = An optional array of `ColumnSpecialization` structs. If you need to -use this with a prepared statement, please use `mysql.prepared.Prepared.columnSpecials`. -+/ -Nullable!MySQLVal queryValue(Connection conn, const(char[]) sql, ColumnSpecialization[] csa = null) -{ - return queryValueImpl(csa, conn, ExecQueryImplInfo(false, sql)); -} -///ditto -Nullable!MySQLVal queryValue(T...)(Connection conn, const(char[]) sql, T args) - if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) -{ - auto prepared = conn.prepare(sql); - prepared.setArgs(args); - return queryValue(conn, prepared); -} -///ditto -Nullable!MySQLVal queryValue(Connection conn, const(char[]) sql, MySQLVal[] args) -{ - auto prepared = conn.prepare(sql); - prepared.setArgs(args); - return queryValue(conn, prepared); -} -///ditto -deprecated("Variant support is deprecated. Use MySQLVal[] instead of Variant[]") -Nullable!MySQLVal queryValue(Connection conn, const(char[]) sql, Variant[] args) @system -{ - auto prepared = conn.prepare(sql); - prepared.setArgs(args); - return queryValue(conn, prepared); -} - -///ditto -Nullable!MySQLVal queryValue(Connection conn, ref Prepared prepared) -{ - auto preparedInfo = conn.registerIfNeeded(prepared.sql); - auto result = queryValueImpl(prepared.columnSpecials, conn, prepared.getExecQueryImplInfo(preparedInfo.statementId)); - prepared._lastInsertID = conn.lastInsertID; // Conceivably, this might be needed when multi-statements are enabled. - return result; -} -///ditto -Nullable!MySQLVal queryValue(T...)(Connection conn, ref Prepared prepared, T args) - if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) -{ - prepared.setArgs(args); - return queryValue(conn, prepared); -} -///ditto -Nullable!MySQLVal queryValue(Connection conn, ref Prepared prepared, MySQLVal[] args) -{ - prepared.setArgs(args); - return queryValue(conn, prepared); -} -///ditto -deprecated("Variant support is deprecated. Use MySQLVal[] instead of Variant[]") -Nullable!MySQLVal queryValue(Connection conn, ref Prepared prepared, Variant[] args) @system -{ - prepared.setArgs(args); - return queryValue(conn, prepared); -} - -///ditto -Nullable!MySQLVal queryValue(Connection conn, ref BackwardCompatPrepared prepared) -{ - auto p = prepared.prepared; - auto result = queryValue(conn, p); - prepared._prepared = p; - return result; -} - -/// Common implementation for `queryValue` overloads. -package Nullable!MySQLVal queryValueImpl(ColumnSpecialization[] csa, Connection conn, - ExecQueryImplInfo info) -{ - auto results = queryImpl(csa, conn, info); - if(results.empty) - return Nullable!MySQLVal(); - else - { - auto row = results.front; - results.close(); - - if(row.length == 0) - return Nullable!MySQLVal(); - else - return Nullable!MySQLVal(row[0]); - } -} - -@("execOverloads") -debug(MYSQLN_TESTS) -unittest -{ - import std.array; - import mysql.connection; - import mysql.test.common; - mixin(scopedCn); - - cn.exec("DROP TABLE IF EXISTS `execOverloads`"); - cn.exec("CREATE TABLE `execOverloads` ( - `i` INTEGER, - `s` VARCHAR(50) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - - immutable prepareSQL = "INSERT INTO `execOverloads` VALUES (?, ?)"; - - // Do the inserts, using exec - - // exec: const(char[]) sql - assert(cn.exec("INSERT INTO `execOverloads` VALUES (1, \"aa\")") == 1); - assert(cn.exec(prepareSQL, 2, "bb") == 1); - assert(cn.exec(prepareSQL, [MySQLVal(3), MySQLVal("cc")]) == 1); - - // exec: prepared sql - auto prepared = cn.prepare(prepareSQL); - prepared.setArgs(4, "dd"); - assert(cn.exec(prepared) == 1); - - assert(cn.exec(prepared, 5, "ee") == 1); - assert(prepared.getArg(0) == 5); - assert(prepared.getArg(1) == "ee"); - - assert(cn.exec(prepared, [MySQLVal(6), MySQLVal("ff")]) == 1); - assert(prepared.getArg(0) == 6); - assert(prepared.getArg(1) == "ff"); - - // exec: bcPrepared sql - auto bcPrepared = cn.prepareBackwardCompatImpl(prepareSQL); - bcPrepared.setArgs(7, "gg"); - assert(cn.exec(bcPrepared) == 1); - assert(bcPrepared.getArg(0) == 7); - assert(bcPrepared.getArg(1) == "gg"); - - // Check results - auto rows = cn.query("SELECT * FROM `execOverloads`").array(); - assert(rows.length == 7); - - assert(rows[0].length == 2); - assert(rows[1].length == 2); - assert(rows[2].length == 2); - assert(rows[3].length == 2); - assert(rows[4].length == 2); - assert(rows[5].length == 2); - assert(rows[6].length == 2); - - assert(rows[0][0] == 1); - assert(rows[0][1] == "aa"); - assert(rows[1][0] == 2); - assert(rows[1][1] == "bb"); - assert(rows[2][0] == 3); - assert(rows[2][1] == "cc"); - assert(rows[3][0] == 4); - assert(rows[3][1] == "dd"); - assert(rows[4][0] == 5); - assert(rows[4][1] == "ee"); - assert(rows[5][0] == 6); - assert(rows[5][1] == "ff"); - assert(rows[6][0] == 7); - assert(rows[6][1] == "gg"); -} - -@("queryOverloads") -debug(MYSQLN_TESTS) -unittest -{ - import std.array; - import mysql.connection; - import mysql.test.common; - mixin(scopedCn); - - cn.exec("DROP TABLE IF EXISTS `queryOverloads`"); - cn.exec("CREATE TABLE `queryOverloads` ( - `i` INTEGER, - `s` VARCHAR(50) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - cn.exec("INSERT INTO `queryOverloads` VALUES (1, \"aa\"), (2, \"bb\"), (3, \"cc\")"); - - immutable prepareSQL = "SELECT * FROM `queryOverloads` WHERE `i`=? AND `s`=?"; - - // Test query - { - Row[] rows; - - // String sql - rows = cn.query("SELECT * FROM `queryOverloads` WHERE `i`=1 AND `s`=\"aa\"").array; - assert(rows.length == 1); - assert(rows[0].length == 2); - assert(rows[0][0] == 1); - assert(rows[0][1] == "aa"); - - rows = cn.query(prepareSQL, 2, "bb").array; - assert(rows.length == 1); - assert(rows[0].length == 2); - assert(rows[0][0] == 2); - assert(rows[0][1] == "bb"); - - rows = cn.query(prepareSQL, [MySQLVal(3), MySQLVal("cc")]).array; - assert(rows.length == 1); - assert(rows[0].length == 2); - assert(rows[0][0] == 3); - assert(rows[0][1] == "cc"); - - // Prepared sql - auto prepared = cn.prepare(prepareSQL); - prepared.setArgs(1, "aa"); - rows = cn.query(prepared).array; - assert(rows.length == 1); - assert(rows[0].length == 2); - assert(rows[0][0] == 1); - assert(rows[0][1] == "aa"); - - rows = cn.query(prepared, 2, "bb").array; - assert(rows.length == 1); - assert(rows[0].length == 2); - assert(rows[0][0] == 2); - assert(rows[0][1] == "bb"); - - rows = cn.query(prepared, [MySQLVal(3), MySQLVal("cc")]).array; - assert(rows.length == 1); - assert(rows[0].length == 2); - assert(rows[0][0] == 3); - assert(rows[0][1] == "cc"); - - // BCPrepared sql - auto bcPrepared = cn.prepareBackwardCompatImpl(prepareSQL); - bcPrepared.setArgs(1, "aa"); - rows = cn.query(bcPrepared).array; - assert(rows.length == 1); - assert(rows[0].length == 2); - assert(rows[0][0] == 1); - assert(rows[0][1] == "aa"); - } - - // Test queryRow - { - Nullable!Row nrow; - // avoid always saying nrow.get - Row row() { return nrow.get; } - - // String sql - nrow = cn.queryRow("SELECT * FROM `queryOverloads` WHERE `i`=1 AND `s`=\"aa\""); - assert(!nrow.isNull); - assert(row.length == 2); - assert(row[0] == 1); - assert(row[1] == "aa"); - - nrow = cn.queryRow(prepareSQL, 2, "bb"); - assert(!nrow.isNull); - assert(row.length == 2); - assert(row[0] == 2); - assert(row[1] == "bb"); - - nrow = cn.queryRow(prepareSQL, [MySQLVal(3), MySQLVal("cc")]); - assert(!nrow.isNull); - assert(row.length == 2); - assert(row[0] == 3); - assert(row[1] == "cc"); - - // Prepared sql - auto prepared = cn.prepare(prepareSQL); - prepared.setArgs(1, "aa"); - nrow = cn.queryRow(prepared); - assert(!nrow.isNull); - assert(row.length == 2); - assert(row[0] == 1); - assert(row[1] == "aa"); - - nrow = cn.queryRow(prepared, 2, "bb"); - assert(!nrow.isNull); - assert(row.length == 2); - assert(row[0] == 2); - assert(row[1] == "bb"); - - nrow = cn.queryRow(prepared, [MySQLVal(3), MySQLVal("cc")]); - assert(!nrow.isNull); - assert(row.length == 2); - assert(row[0] == 3); - assert(row[1] == "cc"); - - // BCPrepared sql - auto bcPrepared = cn.prepareBackwardCompatImpl(prepareSQL); - bcPrepared.setArgs(1, "aa"); - nrow = cn.queryRow(bcPrepared); - assert(!nrow.isNull); - assert(row.length == 2); - assert(row[0] == 1); - assert(row[1] == "aa"); - } - - // Test queryRowTuple - { - int i; - string s; - - // String sql - cn.queryRowTuple("SELECT * FROM `queryOverloads` WHERE `i`=1 AND `s`=\"aa\"", i, s); - assert(i == 1); - assert(s == "aa"); - - // Prepared sql - auto prepared = cn.prepare(prepareSQL); - prepared.setArgs(2, "bb"); - cn.queryRowTuple(prepared, i, s); - assert(i == 2); - assert(s == "bb"); - - // BCPrepared sql - auto bcPrepared = cn.prepareBackwardCompatImpl(prepareSQL); - bcPrepared.setArgs(3, "cc"); - cn.queryRowTuple(bcPrepared, i, s); - assert(i == 3); - assert(s == "cc"); - } - - // Test queryValue - { - Nullable!MySQLVal value; - - // String sql - value = cn.queryValue("SELECT * FROM `queryOverloads` WHERE `i`=1 AND `s`=\"aa\""); - assert(!value.isNull); - assert(value.get.kind != MySQLVal.Kind.Null); - assert(value.get == 1); - - value = cn.queryValue(prepareSQL, 2, "bb"); - assert(!value.isNull); - assert(value.get.kind != MySQLVal.Kind.Null); - assert(value.get == 2); - - value = cn.queryValue(prepareSQL, [MySQLVal(3), MySQLVal("cc")]); - assert(!value.isNull); - assert(value.get.kind != MySQLVal.Kind.Null); - assert(value.get == 3); - - // Prepared sql - auto prepared = cn.prepare(prepareSQL); - prepared.setArgs(1, "aa"); - value = cn.queryValue(prepared); - assert(!value.isNull); - assert(value.get.kind != MySQLVal.Kind.Null); - assert(value.get == 1); - - value = cn.queryValue(prepared, 2, "bb"); - assert(!value.isNull); - assert(value.get.kind != MySQLVal.Kind.Null); - assert(value.get == 2); - - value = cn.queryValue(prepared, [MySQLVal(3), MySQLVal("cc")]); - assert(!value.isNull); - assert(value.get.kind != MySQLVal.Kind.Null); - assert(value.get == 3); - - // BCPrepared sql - auto bcPrepared = cn.prepareBackwardCompatImpl(prepareSQL); - bcPrepared.setArgs(1, "aa"); - value = cn.queryValue(bcPrepared); - assert(!value.isNull); - assert(value.get.kind != MySQLVal.Kind.Null); - assert(value.get == 1); - } -} +version(MySQLSafeMode) + public import mysql.safe.commands; +else + public import mysql.unsafe.commands; diff --git a/source/mysql/connection.d b/source/mysql/connection.d index b7f3d9ff..1d0e1e4b 100644 --- a/source/mysql/connection.d +++ b/source/mysql/connection.d @@ -9,7 +9,7 @@ import std.socket; import std.string; import std.typecons; -import mysql.commands; +import mysql.safe.commands; import mysql.exceptions; import mysql.prepared; import mysql.protocol.comms; @@ -309,14 +309,14 @@ struct BackwardCompatPrepared deprecated("Change 'preparedStmt.query()' to 'conn.query(preparedStmt)'") ResultRange query() { - return .query(_conn, _prepared); + return .query(_conn, _prepared).unsafe; } ///ditto deprecated("Change 'preparedStmt.queryRow()' to 'conn.queryRow(preparedStmt)'") Nullable!Row queryRow() { - return .queryRow(_conn, _prepared); + return .queryRow(_conn, _prepared).unsafe; } ///ditto @@ -328,9 +328,9 @@ struct BackwardCompatPrepared ///ditto deprecated("Change 'preparedStmt.queryValue()' to 'conn.queryValue(preparedStmt)'") - Nullable!MySQLVal queryValue() + Nullable!Variant queryValue() @system { - return .queryValue(_conn, _prepared); + return .queryValue(_conn, _prepared).asVariant; } } diff --git a/source/mysql/metadata.d b/source/mysql/metadata.d index f16a3ada..34d5cb86 100644 --- a/source/mysql/metadata.d +++ b/source/mysql/metadata.d @@ -6,7 +6,7 @@ import std.conv; import std.datetime; import std.exception; -import mysql.commands; +import mysql.safe.commands; import mysql.exceptions; import mysql.protocol.sockets; import mysql.result; diff --git a/source/mysql/prepared.d b/source/mysql/prepared.d index cbe496c1..efb0d52c 100644 --- a/source/mysql/prepared.d +++ b/source/mysql/prepared.d @@ -7,7 +7,7 @@ import std.traits; import std.typecons; import std.variant; -import mysql.commands; +import mysql.safe.commands; import mysql.exceptions; import mysql.protocol.comms; import mysql.protocol.constants; @@ -241,7 +241,6 @@ public: setArg(index, val.get(), psn); } - deprecated("Using Variant is deprecated, please use MySQLVal instead") void setArg(T)(size_t index, T val, ParameterSpecialization psn = PSN(0, SQLType.INFER_FROM_D_TYPE, 0, null)) @system if(is(T == Variant)) { @@ -362,7 +361,6 @@ public: } /// ditto - deprecated("Using Variant is deprecated, please use MySQLVal instead") void setArgs(Variant[] args, ParameterSpecialization[] psnList=null) @system { enforce!MYX(args.length == _numParams, "Param count supplied does not match prepared statement"); @@ -393,7 +391,6 @@ public: } /// ditto - deprecated("Using Variant is deprecated, please use getArg instead") Variant vGetArg(size_t index) @system { // convert to Variant. @@ -438,7 +435,7 @@ public: immutable selectSQL = "SELECT * FROM `setNullArg`"; auto preparedInsert = cn.prepare(insertSQL); assert(preparedInsert.sql == insertSQL); - Row[] rs; + SafeRow[] rs; { Nullable!int nullableInt; diff --git a/source/mysql/protocol/comms.d b/source/mysql/protocol/comms.d index 3950aefd..634df0d5 100644 --- a/source/mysql/protocol/comms.d +++ b/source/mysql/protocol/comms.d @@ -809,7 +809,7 @@ package(mysql) ubyte[] makeToken(string password, ubyte[] authBuf) } /// Get the next `mysql.result.Row` of a pending result set. -package(mysql) Row getNextRow(Connection conn) +package(mysql) SafeRow getNextRow(Connection conn) { scope(failure) conn.kill(); @@ -819,7 +819,7 @@ package(mysql) Row getNextRow(Connection conn) conn._headersPending = false; } ubyte[] packet; - Row rr; + SafeRow rr; packet = conn.getPacket(); if(packet.front == ResultPacketMarker.error) throw new MYXReceived(OKErrorPacket(packet), __FILE__, __LINE__); @@ -830,9 +830,9 @@ package(mysql) Row getNextRow(Connection conn) return rr; } if (conn._binaryPending) - rr = Row(conn, packet, conn._rsh, true); + rr = SafeRow(conn, packet, conn._rsh, true); else - rr = Row(conn, packet, conn._rsh, false); + rr = SafeRow(conn, packet, conn._rsh, false); //rr._valid = true; return rr; } diff --git a/source/mysql/result.d b/source/mysql/result.d index 47620298..4e41c5dd 100644 --- a/source/mysql/result.d +++ b/source/mysql/result.d @@ -5,7 +5,6 @@ import std.conv; import std.exception; import std.range; import std.string; -import std.variant; import mysql.connection; import mysql.exceptions; @@ -13,6 +12,8 @@ import mysql.protocol.comms; import mysql.protocol.extra_types; import mysql.protocol.packets; public import mysql.types; +import std.typecons : Nullable; +import std.variant; /++ A struct to represent a single row of a result set. @@ -28,7 +29,7 @@ I have been agitating for some kind of null indicator that can be set for a Variant without destroying its inherent type information. If this were the case, then the bool array could disappear. +/ -struct Row +struct SafeRow { import mysql.connection; @@ -88,7 +89,7 @@ public: unittest { import mysql.test.common; - import mysql.commands; + import mysql.safe.commands; mixin(scopedCn); cn.exec("DROP TABLE IF EXISTS `row_getName`"); cn.exec("CREATE TABLE `row_getName` (someValue INTEGER, another INTEGER) ENGINE=InnoDB DEFAULT CHARSET=utf8"); @@ -177,21 +178,41 @@ public: } /// ditto -deprecated("Usage of Variant is deprecated. Please switch code to use safe MySQLVal types") -UnsafeRow unsafe(Row r) +struct UnsafeRow { - return UnsafeRow(r); + SafeRow _safe; + alias _safe this; + Variant opIndex(size_t idx) { + return _safe[idx].asVariant; + } } /// ditto -struct UnsafeRow +UnsafeRow unsafe(SafeRow r) @safe { - Row safe; - alias safe this; - deprecated("Variant support is deprecated. Please switch to using MySQLVal") - Variant opIndex(size_t idx) { - return safe[idx].asVariant; - } + return Row(r); +} + +/// ditto +Nullable!UnsafeRow unsafe(Nullable!SafeRow r) @safe +{ + if(r.isNull) + return Nullable!UnsafeRow(); + return Nullable!UnsafeRow(r.get.unsafe); +} + + +SafeRow safe(UnsafeRow r) @safe +{ + return r._safe; +} + + +Nullable!SafeRow safe(Nullable!UnsafeRow r) @safe +{ + if(r.isNull) + return Nullable!SafeRow(); + return Nullable!SafeRow(r.get.safe); } /++ @@ -224,13 +245,13 @@ ResultRange oneAtATime = myConnection.query("SELECT * from myTable"); Row[] allAtOnce = myConnection.query("SELECT * from myTable").array; --- +/ -struct ResultRange +struct SafeResultRange { private: @safe: Connection _con; ResultSetHeaders _rsh; - Row _row; // current row + SafeRow _row; // current row string[] _colNames; size_t[string] _colNameIndicies; ulong _numRowsFetched; @@ -280,7 +301,7 @@ public: /++ Gets the current row +/ - @property inout(Row) front() pure inout + @property inout(SafeRow) front() pure inout { ensureValid(); enforce!MYX(!empty, "Attempted 'front' on exhausted result sequence."); @@ -345,21 +366,13 @@ public: @property ulong rowCount() const pure nothrow { return _numRowsFetched; } } -/// ditto -deprecated("Usage of Variant is deprecated. Please switch code to use safe MySQLVal types") -auto unsafe(ResultRange r) -{ - return UnsafeResultRange(r); -} - /// ditto struct UnsafeResultRange { - ResultRange safe; + SafeResultRange safe; alias safe this; - inout(UnsafeRow) front() inout { return inout(UnsafeRow)(safe.front); } + inout(Row) front() inout { return inout(Row)(safe.front); } - deprecated("Variant support is deprecated. Please switch to using MySQLVal") Variant[string] asAA() { ensureValid(); @@ -370,3 +383,20 @@ struct UnsafeResultRange return aa; } } + +/// ditto +UnsafeResultRange unsafe(SafeResultRange r) @safe +{ + return UnsafeResultRange(r); +} + +version(MySQLSafeMode) +{ + alias Row = SafeRow; + alias ResultRange = SafeResultRange; +} +else +{ + alias Row = UnsafeRow; + alias ResultRange = UnsafeResultRange; +} diff --git a/source/mysql/safe/commands.d b/source/mysql/safe/commands.d new file mode 100644 index 00000000..548f745a --- /dev/null +++ b/source/mysql/safe/commands.d @@ -0,0 +1,1004 @@ +/++ +Use a DB via plain SQL statements. + +Commands that are expected to return a result set - queries - have distinctive +methods that are enforced. That is it will be an error to call such a method +with an SQL command that does not produce a result set. So for commands like +SELECT, use the `query` functions. For other commands, like +INSERT/UPDATE/CREATE/etc, use `exec`. ++/ + +module mysql.safe.commands; + +import std.conv; +import std.exception; +import std.range; +import std.typecons; +import std.variant; + +import mysql.connection; +import mysql.exceptions; +import mysql.prepared; +import mysql.protocol.comms; +import mysql.protocol.constants; +import mysql.protocol.extra_types; +import mysql.protocol.packets; +import mysql.result; +import mysql.types; + +/// This feature is not yet implemented. It currently has no effect. +/+ +A struct to represent specializations of returned statement columns. + +If you are executing a query that will include result columns that are large objects, +it may be expedient to deal with the data as it is received rather than first buffering +it to some sort of byte array. These two variables allow for this. If both are provided +then the corresponding column will be fed to the stipulated delegate in chunks of +`chunkSize`, with the possible exception of the last chunk, which may be smaller. +The bool argument `finished` will be set to true when the last chunk is set. + +Be aware when specifying types for column specializations that for some reason the +field descriptions returned for a resultset have all of the types TINYTEXT, MEDIUMTEXT, +TEXT, LONGTEXT, TINYBLOB, MEDIUMBLOB, BLOB, and LONGBLOB lumped as type 0xfc +contrary to what it says in the protocol documentation. ++/ +struct ColumnSpecialization +{ + size_t cIndex; // parameter number 0 - number of params-1 + ushort type; + uint chunkSize; /// In bytes + void delegate(const(ubyte)[] chunk, bool finished) @safe chunkDelegate; +} +///ditto +alias CSN = ColumnSpecialization; + +@safe: + +@("columnSpecial") +debug(MYSQLN_TESTS) +unittest +{ + import std.array; + import std.range; + import mysql.test.common; + mixin(scopedCn); + + // Setup + cn.exec("DROP TABLE IF EXISTS `columnSpecial`"); + cn.exec("CREATE TABLE `columnSpecial` ( + `data` LONGBLOB + ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + + immutable totalSize = 1000; // Deliberately not a multiple of chunkSize below + auto alph = cast(const(ubyte)[]) "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + auto data = alph.cycle.take(totalSize).array; + cn.exec("INSERT INTO `columnSpecial` VALUES (\""~(cast(const(char)[])data)~"\")"); + + // Common stuff + int chunkSize; + immutable selectSQL = "SELECT `data` FROM `columnSpecial`"; + ubyte[] received; + bool lastValueOfFinished; + void receiver(const(ubyte)[] chunk, bool finished) @safe + { + assert(lastValueOfFinished == false); + + if(finished) + assert(chunk.length == chunkSize); + else + assert(chunk.length < chunkSize); // Not always true in general, but true in this unittest + + received ~= chunk; + lastValueOfFinished = finished; + } + + // Sanity check + auto value = cn.queryValue(selectSQL); + assert(!value.isNull); + assert(value.get == data); + + // Use ColumnSpecialization with sql string, + // and totalSize as a multiple of chunkSize + { + chunkSize = 100; + assert(cast(int)(totalSize / chunkSize) * chunkSize == totalSize); + auto columnSpecial = ColumnSpecialization(0, 0xfc, chunkSize, &receiver); + + received = null; + lastValueOfFinished = false; + value = cn.queryValue(selectSQL, [columnSpecial]); + assert(!value.isNull); + assert(value.get == data); + //TODO: ColumnSpecialization is not yet implemented + //assert(lastValueOfFinished == true); + //assert(received == data); + } + + // Use ColumnSpecialization with sql string, + // and totalSize as a non-multiple of chunkSize + { + chunkSize = 64; + assert(cast(int)(totalSize / chunkSize) * chunkSize != totalSize); + auto columnSpecial = ColumnSpecialization(0, 0xfc, chunkSize, &receiver); + + received = null; + lastValueOfFinished = false; + value = cn.queryValue(selectSQL, [columnSpecial]); + assert(!value.isNull); + assert(value.get == data); + //TODO: ColumnSpecialization is not yet implemented + //assert(lastValueOfFinished == true); + //assert(received == data); + } + + // Use ColumnSpecialization with prepared statement, + // and totalSize as a multiple of chunkSize + { + chunkSize = 100; + assert(cast(int)(totalSize / chunkSize) * chunkSize == totalSize); + auto columnSpecial = ColumnSpecialization(0, 0xfc, chunkSize, &receiver); + + received = null; + lastValueOfFinished = false; + auto prepared = cn.prepare(selectSQL); + prepared.columnSpecials = [columnSpecial]; + value = cn.queryValue(prepared); + assert(!value.isNull); + assert(value.get == data); + //TODO: ColumnSpecialization is not yet implemented + //assert(lastValueOfFinished == true); + //assert(received == data); + } +} + +/++ +Execute an SQL command or prepared statement, such as INSERT/UPDATE/CREATE/etc. + +This method is intended for commands such as which do not produce a result set +(otherwise, use one of the `query` functions instead.) If the SQL command does +produces a result set (such as SELECT), `mysql.exceptions.MYXResultRecieved` +will be thrown. + +If `args` is supplied, the sql string will automatically be used as a prepared +statement. Prepared statements are automatically cached by mysql-native, +so there's no performance penalty for using this multiple times for the +same statement instead of manually preparing a statement. + +If `args` and `prepared` are both provided, `args` will be used, +and any arguments that are already set in the prepared statement +will automatically be replaced with `args` (note, just like calling +`mysql.prepared.Prepared.setArgs`, this will also remove all +`mysql.prepared.ParameterSpecialization` that may have been applied). + +Only use the `const(char[]) sql` overload that doesn't take `args` +when you are not going to be using the same +command repeatedly and you are CERTAIN all the data you're sending is properly +escaped. Otherwise, consider using overload that takes a `Prepared`. + +If you need to use any `mysql.prepared.ParameterSpecialization`, use +`mysql.connection.prepare` to manually create a `mysql.prepared.Prepared`, +and set your parameter specializations using `mysql.prepared.Prepared.setArg` +or `mysql.prepared.Prepared.setArgs`. + +Type_Mappings: $(TYPE_MAPPINGS) + +Params: +conn = An open `mysql.connection.Connection` to the database. +sql = The SQL command to be run. +prepared = The prepared statement to be run. + +Returns: The number of rows affected. + +Example: +--- +auto myInt = 7; +auto rowsAffected = myConnection.exec("INSERT INTO `myTable` (`a`) VALUES (?)", myInt); +--- ++/ +ulong exec(Connection conn, const(char[]) sql) +{ + return execImpl(conn, ExecQueryImplInfo(false, sql)); +} +///ditto +ulong exec(T...)(Connection conn, const(char[]) sql, T args) + if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[])) +{ + auto prepared = conn.prepare(sql); + prepared.setArgs(args); + return exec(conn, prepared); +} +///ditto +ulong exec(Connection conn, const(char[]) sql, MySQLVal[] args) +{ + auto prepared = conn.prepare(sql); + prepared.setArgs(args); + return exec(conn, prepared); +} + +///ditto +ulong exec(Connection conn, ref Prepared prepared) +{ + auto preparedInfo = conn.registerIfNeeded(prepared.sql); + auto ra = execImpl(conn, prepared.getExecQueryImplInfo(preparedInfo.statementId)); + prepared._lastInsertID = conn.lastInsertID; + return ra; +} +///ditto +ulong exec(T...)(Connection conn, ref Prepared prepared, T args) + if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[])) +{ + prepared.setArgs(args); + return exec(conn, prepared); +} + +///ditto +ulong exec(Connection conn, ref Prepared prepared, MySQLVal[] args) +{ + prepared.setArgs(args); + return exec(conn, prepared); +} + +///ditto +ulong exec(Connection conn, ref BackwardCompatPrepared prepared) +{ + auto p = prepared.prepared; + auto result = exec(conn, p); + prepared._prepared = p; + return result; +} + +/// Common implementation for `exec` overloads +package ulong execImpl(Connection conn, ExecQueryImplInfo info) +{ + ulong rowsAffected; + bool receivedResultSet = execQueryImpl(conn, info, rowsAffected); + if(receivedResultSet) + { + conn.purgeResult(); + throw new MYXResultRecieved(); + } + + return rowsAffected; +} + +/++ +Execute an SQL SELECT command or prepared statement. + +This returns an input range of `mysql.result.Row`, so if you need random access +to the `mysql.result.Row` elements, simply call +$(LINK2 https://dlang.org/phobos/std_array.html#array, `std.array.array()`) +on the result. + +If the SQL command does not produce a result set (such as INSERT/CREATE/etc), +then `mysql.exceptions.MYXNoResultRecieved` will be thrown. Use +`exec` instead for such commands. + +If `args` is supplied, the sql string will automatically be used as a prepared +statement. Prepared statements are automatically cached by mysql-native, +so there's no performance penalty for using this multiple times for the +same statement instead of manually preparing a statement. + +If `args` and `prepared` are both provided, `args` will be used, +and any arguments that are already set in the prepared statement +will automatically be replaced with `args` (note, just like calling +`mysql.prepared.Prepared.setArgs`, this will also remove all +`mysql.prepared.ParameterSpecialization` that may have been applied). + +Only use the `const(char[]) sql` overload that doesn't take `args` +when you are not going to be using the same +command repeatedly and you are CERTAIN all the data you're sending is properly +escaped. Otherwise, consider using overload that takes a `Prepared`. + +If you need to use any `mysql.prepared.ParameterSpecialization`, use +`mysql.connection.prepare` to manually create a `mysql.prepared.Prepared`, +and set your parameter specializations using `mysql.prepared.Prepared.setArg` +or `mysql.prepared.Prepared.setArgs`. + +Type_Mappings: $(TYPE_MAPPINGS) + +Params: +conn = An open `mysql.connection.Connection` to the database. +sql = The SQL command to be run. +prepared = The prepared statement to be run. +csa = Not yet implemented. + +Returns: A (possibly empty) `mysql.result.ResultRange`. + +Example: +--- +ResultRange oneAtATime = myConnection.query("SELECT * from `myTable`"); +Row[] allAtOnce = myConnection.query("SELECT * from `myTable`").array; + +auto myInt = 7; +ResultRange rows = myConnection.query("SELECT * FROM `myTable` WHERE `a` = ?", myInt); +--- ++/ +/+ +Future text: +If there are long data items among the expected result columns you can use +the `csa` param to specify that they are to be subject to chunked transfer via a +delegate. + +csa = An optional array of `ColumnSpecialization` structs. If you need to +use this with a prepared statement, please use `mysql.prepared.Prepared.columnSpecials`. ++/ +SafeResultRange query(Connection conn, const(char[]) sql, ColumnSpecialization[] csa = null) +{ + return queryImpl(csa, conn, ExecQueryImplInfo(false, sql)); +} +///ditto +SafeResultRange query(T...)(Connection conn, const(char[]) sql, T args) + if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) +{ + auto prepared = conn.prepare(sql); + prepared.setArgs(args); + return query(conn, prepared); +} +///ditto +SafeResultRange query(Connection conn, const(char[]) sql, Variant[] args) @system +{ + auto prepared = conn.prepare(sql); + prepared.setArgs(args); + return query(conn, prepared); +} + +///ditto +SafeResultRange query(Connection conn, const(char[]) sql, MySQLVal[] args) +{ + auto prepared = conn.prepare(sql); + prepared.setArgs(args); + return query(conn, prepared); +} + +///ditto +SafeResultRange query(Connection conn, ref Prepared prepared) +{ + auto preparedInfo = conn.registerIfNeeded(prepared.sql); + auto result = queryImpl(prepared.columnSpecials, conn, prepared.getExecQueryImplInfo(preparedInfo.statementId)); + prepared._lastInsertID = conn.lastInsertID; // Conceivably, this might be needed when multi-statements are enabled. + return result; +} +///ditto +SafeResultRange query(T...)(Connection conn, ref Prepared prepared, T args) + if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) +{ + prepared.setArgs(args); + return query(conn, prepared); +} +///ditto +SafeResultRange query(Connection conn, ref Prepared prepared, MySQLVal[] args) +{ + prepared.setArgs(args); + return query(conn, prepared); +} + +/// Common implementation for `query` overloads +package SafeResultRange queryImpl(ColumnSpecialization[] csa, + Connection conn, ExecQueryImplInfo info) +{ + ulong ra; + enforce!MYXNoResultRecieved(execQueryImpl(conn, info, ra)); + + conn._rsh = ResultSetHeaders(conn, conn._fieldCount); + if(csa !is null) + conn._rsh.addSpecializations(csa); + + conn._headersPending = false; + return SafeResultRange(conn, conn._rsh, conn._rsh.fieldNames); +} + +/++ +Execute an SQL SELECT command or prepared statement where you only want the +first `mysql.result.Row`, if any. + +If the SQL command does not produce a result set (such as INSERT/CREATE/etc), +then `mysql.exceptions.MYXNoResultRecieved` will be thrown. Use +`exec` instead for such commands. + +If `args` is supplied, the sql string will automatically be used as a prepared +statement. Prepared statements are automatically cached by mysql-native, +so there's no performance penalty for using this multiple times for the +same statement instead of manually preparing a statement. + +If `args` and `prepared` are both provided, `args` will be used, +and any arguments that are already set in the prepared statement +will automatically be replaced with `args` (note, just like calling +`mysql.prepared.Prepared.setArgs`, this will also remove all +`mysql.prepared.ParameterSpecialization` that may have been applied). + +Only use the `const(char[]) sql` overload that doesn't take `args` +when you are not going to be using the same +command repeatedly and you are CERTAIN all the data you're sending is properly +escaped. Otherwise, consider using overload that takes a `Prepared`. + +If you need to use any `mysql.prepared.ParameterSpecialization`, use +`mysql.connection.prepare` to manually create a `mysql.prepared.Prepared`, +and set your parameter specializations using `mysql.prepared.Prepared.setArg` +or `mysql.prepared.Prepared.setArgs`. + +Type_Mappings: $(TYPE_MAPPINGS) + +Params: +conn = An open `mysql.connection.Connection` to the database. +sql = The SQL command to be run. +prepared = The prepared statement to be run. +csa = Not yet implemented. + +Returns: `Nullable!(mysql.result.Row)`: This will be null (check via `Nullable.isNull`) if the +query resulted in an empty result set. + +Example: +--- +auto myInt = 7; +Nullable!Row row = myConnection.queryRow("SELECT * FROM `myTable` WHERE `a` = ?", myInt); +--- ++/ +/+ +Future text: +If there are long data items among the expected result columns you can use +the `csa` param to specify that they are to be subject to chunked transfer via a +delegate. + +csa = An optional array of `ColumnSpecialization` structs. If you need to +use this with a prepared statement, please use `mysql.prepared.Prepared.columnSpecials`. ++/ +/+ +Future text: +If there are long data items among the expected result columns you can use +the `csa` param to specify that they are to be subject to chunked transfer via a +delegate. + +csa = An optional array of `ColumnSpecialization` structs. If you need to +use this with a prepared statement, please use `mysql.prepared.Prepared.columnSpecials`. ++/ +Nullable!SafeRow queryRow(Connection conn, const(char[]) sql, ColumnSpecialization[] csa = null) +{ + return queryRowImpl(csa, conn, ExecQueryImplInfo(false, sql)); +} +///ditto +Nullable!SafeRow queryRow(T...)(Connection conn, const(char[]) sql, T args) + if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) +{ + auto prepared = conn.prepare(sql); + prepared.setArgs(args); + return queryRow(conn, prepared); +} +///ditto +Nullable!SafeRow queryRow(Connection conn, const(char[]) sql, MySQLVal[] args) +{ + auto prepared = conn.prepare(sql); + prepared.setArgs(args); + return queryRow(conn, prepared); +} + +///ditto +Nullable!SafeRow queryRow(Connection conn, ref Prepared prepared) +{ + auto preparedInfo = conn.registerIfNeeded(prepared.sql); + auto result = queryRowImpl(prepared.columnSpecials, conn, prepared.getExecQueryImplInfo(preparedInfo.statementId)); + prepared._lastInsertID = conn.lastInsertID; // Conceivably, this might be needed when multi-statements are enabled. + return result; +} +///ditto +Nullable!SafeRow queryRow(T...)(Connection conn, ref Prepared prepared, T args) + if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) +{ + prepared.setArgs(args); + return queryRow(conn, prepared); +} +///ditto +Nullable!SafeRow queryRow(Connection conn, ref Prepared prepared, MySQLVal[] args) +{ + prepared.setArgs(args); + return queryRow(conn, prepared); +} + +///ditto +Nullable!SafeRow queryRow(Connection conn, ref BackwardCompatPrepared prepared) +{ + auto p = prepared.prepared; + auto result = queryRow(conn, p); + prepared._prepared = p; + return result; +} + +/// Common implementation for `querySet` overloads. +package Nullable!SafeRow queryRowImpl(ColumnSpecialization[] csa, Connection conn, + ExecQueryImplInfo info) +{ + auto results = queryImpl(csa, conn, info); + if(results.empty) + return Nullable!SafeRow(); + else + { + auto row = results.front; + results.close(); + return Nullable!SafeRow(row); + } +} + +/++ +Execute an SQL SELECT command or prepared statement where you only want the +first `mysql.result.Row`, and place result values into a set of D variables. + +This method will throw if any column type is incompatible with the corresponding D variable. + +Unlike the other query functions, queryRowTuple will throw +`mysql.exceptions.MYX` if the result set is empty +(and thus the reference variables passed in cannot be filled). + +If the SQL command does not produce a result set (such as INSERT/CREATE/etc), +then `mysql.exceptions.MYXNoResultRecieved` will be thrown. Use +`exec` instead for such commands. + +Only use the `const(char[]) sql` overload when you are not going to be using the same +command repeatedly and you are CERTAIN all the data you're sending is properly +escaped. Otherwise, consider using overload that takes a `Prepared`. + +Type_Mappings: $(TYPE_MAPPINGS) + +Params: +conn = An open `mysql.connection.Connection` to the database. +sql = The SQL command to be run. +prepared = The prepared statement to be run. +args = The variables, taken by reference, to receive the values. ++/ +void queryRowTuple(T...)(Connection conn, const(char[]) sql, ref T args) +{ + return queryRowTupleImpl(conn, ExecQueryImplInfo(false, sql), args); +} + +///ditto +void queryRowTuple(T...)(Connection conn, ref Prepared prepared, ref T args) +{ + auto preparedInfo = conn.registerIfNeeded(prepared.sql); + queryRowTupleImpl(conn, prepared.getExecQueryImplInfo(preparedInfo.statementId), args); + prepared._lastInsertID = conn.lastInsertID; // Conceivably, this might be needed when multi-statements are enabled. +} + +/// Common implementation for `queryRowTuple` overloads. +package void queryRowTupleImpl(T...)(Connection conn, ExecQueryImplInfo info, ref T args) +{ + ulong ra; + enforce!MYXNoResultRecieved(execQueryImpl(conn, info, ra)); + + auto rr = conn.getNextRow(); + /+if (!rr._valid) // The result set was empty - not a crime. + return;+/ + enforce!MYX(rr._values.length == args.length, "Result column count does not match the target tuple."); + foreach (size_t i, dummy; args) + { + import taggedalgebraic.taggedalgebraic : get, hasType; + enforce!MYX(rr._values[i].hasType!(T[i]), + "Tuple "~to!string(i)~" type and column type are not compatible."); + // use taggedalgebraic get to avoid extra calls. + args[i] = get!(T[i])(rr._values[i]); + } + // If there were more rows, flush them away + // Question: Should I check in purgeResult and throw if there were - it's very inefficient to + // allow sloppy SQL that does not ensure just one row! + conn.purgeResult(); +} + +// Test what happends when queryRowTuple receives no rows +@("queryRowTuple_noRows") +debug(MYSQLN_TESTS) +unittest +{ + import mysql.test.common : scopedCn, createCn; + mixin(scopedCn); + + cn.exec("DROP TABLE IF EXISTS `queryRowTuple_noRows`"); + cn.exec("CREATE TABLE `queryRowTuple_noRows` ( + `val` INTEGER + ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + + immutable selectSQL = "SELECT * FROM `queryRowTuple_noRows`"; + int queryTupleResult; + assertThrown!MYX(cn.queryRowTuple(selectSQL, queryTupleResult)); +} + +/++ +Execute an SQL SELECT command or prepared statement and return a single value: +the first column of the first row received. + +If the query did not produce any rows, or the rows it produced have zero columns, +this will return `Nullable!Variant()`, ie, null. Test for this with `result.isNull`. + +If the query DID produce a result, but the value actually received is NULL, +then `result.isNull` will be FALSE, and `result.get` will produce a Variant +which CONTAINS null. Check for this with `result.get.type == typeid(typeof(null))`. + +If the SQL command does not produce a result set (such as INSERT/CREATE/etc), +then `mysql.exceptions.MYXNoResultRecieved` will be thrown. Use +`exec` instead for such commands. + +If `args` is supplied, the sql string will automatically be used as a prepared +statement. Prepared statements are automatically cached by mysql-native, +so there's no performance penalty for using this multiple times for the +same statement instead of manually preparing a statement. + +If `args` and `prepared` are both provided, `args` will be used, +and any arguments that are already set in the prepared statement +will automatically be replaced with `args` (note, just like calling +`mysql.prepared.Prepared.setArgs`, this will also remove all +`mysql.prepared.ParameterSpecialization` that may have been applied). + +Only use the `const(char[]) sql` overload that doesn't take `args` +when you are not going to be using the same +command repeatedly and you are CERTAIN all the data you're sending is properly +escaped. Otherwise, consider using overload that takes a `Prepared`. + +If you need to use any `mysql.prepared.ParameterSpecialization`, use +`mysql.connection.prepare` to manually create a `mysql.prepared.Prepared`, +and set your parameter specializations using `mysql.prepared.Prepared.setArg` +or `mysql.prepared.Prepared.setArgs`. + +Type_Mappings: $(TYPE_MAPPINGS) + +Params: +conn = An open `mysql.connection.Connection` to the database. +sql = The SQL command to be run. +prepared = The prepared statement to be run. +csa = Not yet implemented. + +Returns: `Nullable!Variant`: This will be null (check via `Nullable.isNull`) if the +query resulted in an empty result set. + +Example: +--- +auto myInt = 7; +Nullable!Variant value = myConnection.queryRow("SELECT * FROM `myTable` WHERE `a` = ?", myInt); +--- ++/ +/+ +Future text: +If there are long data items among the expected result columns you can use +the `csa` param to specify that they are to be subject to chunked transfer via a +delegate. + +csa = An optional array of `ColumnSpecialization` structs. If you need to +use this with a prepared statement, please use `mysql.prepared.Prepared.columnSpecials`. ++/ +/+ +Future text: +If there are long data items among the expected result columns you can use +the `csa` param to specify that they are to be subject to chunked transfer via a +delegate. + +csa = An optional array of `ColumnSpecialization` structs. If you need to +use this with a prepared statement, please use `mysql.prepared.Prepared.columnSpecials`. ++/ +///ditto +Nullable!MySQLVal queryValue(Connection conn, const(char[]) sql, ColumnSpecialization[] csa = null) +{ + return queryValueImpl(csa, conn, ExecQueryImplInfo(false, sql)); +} +///ditto +Nullable!MySQLVal queryValue(T...)(Connection conn, const(char[]) sql, T args) + if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) +{ + auto prepared = conn.prepare(sql); + prepared.setArgs(args); + return queryValue(conn, prepared); +} +///ditto +Nullable!MySQLVal queryValue(Connection conn, const(char[]) sql, MySQLVal[] args) +{ + auto prepared = conn.prepare(sql); + prepared.setArgs(args); + return queryValue(conn, prepared); +} +///ditto +Nullable!MySQLVal queryValue(Connection conn, ref Prepared prepared) +{ + auto preparedInfo = conn.registerIfNeeded(prepared.sql); + auto result = queryValueImpl(prepared.columnSpecials, conn, prepared.getExecQueryImplInfo(preparedInfo.statementId)); + prepared._lastInsertID = conn.lastInsertID; // Conceivably, this might be needed when multi-statements are enabled. + return result; +} +///ditto +Nullable!MySQLVal queryValue(T...)(Connection conn, ref Prepared prepared, T args) + if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) +{ + prepared.setArgs(args); + return queryValue(conn, prepared); +} +///ditto +Nullable!MySQLVal queryValue(Connection conn, ref Prepared prepared, MySQLVal[] args) +{ + prepared.setArgs(args); + return queryValue(conn, prepared); +} + +/// Common implementation for `queryValue` overloads. +package Nullable!MySQLVal queryValueImpl(ColumnSpecialization[] csa, Connection conn, + ExecQueryImplInfo info) +{ + auto results = queryImpl(csa, conn, info); + if(results.empty) + return Nullable!MySQLVal(); + else + { + auto row = results.front; + results.close(); + + if(row.length == 0) + return Nullable!MySQLVal(); + else + return Nullable!MySQLVal(row[0]); + } +} + +@("execOverloads") +debug(MYSQLN_TESTS) +unittest +{ + import std.array; + import mysql.connection; + import mysql.test.common; + mixin(scopedCn); + + cn.exec("DROP TABLE IF EXISTS `execOverloads`"); + cn.exec("CREATE TABLE `execOverloads` ( + `i` INTEGER, + `s` VARCHAR(50) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + + immutable prepareSQL = "INSERT INTO `execOverloads` VALUES (?, ?)"; + + // Do the inserts, using exec + + // exec: const(char[]) sql + assert(cn.exec("INSERT INTO `execOverloads` VALUES (1, \"aa\")") == 1); + assert(cn.exec(prepareSQL, 2, "bb") == 1); + assert(cn.exec(prepareSQL, [MySQLVal(3), MySQLVal("cc")]) == 1); + + // exec: prepared sql + auto prepared = cn.prepare(prepareSQL); + prepared.setArgs(4, "dd"); + assert(cn.exec(prepared) == 1); + + assert(cn.exec(prepared, 5, "ee") == 1); + assert(prepared.getArg(0) == 5); + assert(prepared.getArg(1) == "ee"); + + assert(cn.exec(prepared, [MySQLVal(6), MySQLVal("ff")]) == 1); + assert(prepared.getArg(0) == 6); + assert(prepared.getArg(1) == "ff"); + + // exec: bcPrepared sql + auto bcPrepared = cn.prepareBackwardCompatImpl(prepareSQL); + bcPrepared.setArgs(7, "gg"); + assert(cn.exec(bcPrepared) == 1); + assert(bcPrepared.getArg(0) == 7); + assert(bcPrepared.getArg(1) == "gg"); + + // Check results + auto rows = cn.query("SELECT * FROM `execOverloads`").array(); + assert(rows.length == 7); + + assert(rows[0].length == 2); + assert(rows[1].length == 2); + assert(rows[2].length == 2); + assert(rows[3].length == 2); + assert(rows[4].length == 2); + assert(rows[5].length == 2); + assert(rows[6].length == 2); + + assert(rows[0][0] == 1); + assert(rows[0][1] == "aa"); + assert(rows[1][0] == 2); + assert(rows[1][1] == "bb"); + assert(rows[2][0] == 3); + assert(rows[2][1] == "cc"); + assert(rows[3][0] == 4); + assert(rows[3][1] == "dd"); + assert(rows[4][0] == 5); + assert(rows[4][1] == "ee"); + assert(rows[5][0] == 6); + assert(rows[5][1] == "ff"); + assert(rows[6][0] == 7); + assert(rows[6][1] == "gg"); +} + +@("queryOverloads") +debug(MYSQLN_TESTS) +unittest +{ + import std.array; + import mysql.connection; + import mysql.test.common; + mixin(scopedCn); + + cn.exec("DROP TABLE IF EXISTS `queryOverloads`"); + cn.exec("CREATE TABLE `queryOverloads` ( + `i` INTEGER, + `s` VARCHAR(50) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + cn.exec("INSERT INTO `queryOverloads` VALUES (1, \"aa\"), (2, \"bb\"), (3, \"cc\")"); + + immutable prepareSQL = "SELECT * FROM `queryOverloads` WHERE `i`=? AND `s`=?"; + + // Test query + { + SafeRow[] rows; + + // String sql + rows = cn.query("SELECT * FROM `queryOverloads` WHERE `i`=1 AND `s`=\"aa\"").array; + assert(rows.length == 1); + assert(rows[0].length == 2); + assert(rows[0][0] == 1); + assert(rows[0][1] == "aa"); + + rows = cn.query(prepareSQL, 2, "bb").array; + assert(rows.length == 1); + assert(rows[0].length == 2); + assert(rows[0][0] == 2); + assert(rows[0][1] == "bb"); + + rows = cn.query(prepareSQL, [MySQLVal(3), MySQLVal("cc")]).array; + assert(rows.length == 1); + assert(rows[0].length == 2); + assert(rows[0][0] == 3); + assert(rows[0][1] == "cc"); + + // Prepared sql + auto prepared = cn.prepare(prepareSQL); + prepared.setArgs(1, "aa"); + rows = cn.query(prepared).array; + assert(rows.length == 1); + assert(rows[0].length == 2); + assert(rows[0][0] == 1); + assert(rows[0][1] == "aa"); + + rows = cn.query(prepared, 2, "bb").array; + assert(rows.length == 1); + assert(rows[0].length == 2); + assert(rows[0][0] == 2); + assert(rows[0][1] == "bb"); + + rows = cn.query(prepared, [MySQLVal(3), MySQLVal("cc")]).array; + assert(rows.length == 1); + assert(rows[0].length == 2); + assert(rows[0][0] == 3); + assert(rows[0][1] == "cc"); + + // BCPrepared sql + auto bcPrepared = cn.prepareBackwardCompatImpl(prepareSQL); + bcPrepared.setArgs(1, "aa"); + rows = cn.query(bcPrepared).array; + assert(rows.length == 1); + assert(rows[0].length == 2); + assert(rows[0][0] == 1); + assert(rows[0][1] == "aa"); + } + + // Test queryRow + { + Nullable!SafeRow nrow; + // avoid always saying nrow.get + SafeRow row() { return nrow.get; } + + // String sql + nrow = cn.queryRow("SELECT * FROM `queryOverloads` WHERE `i`=1 AND `s`=\"aa\""); + assert(!nrow.isNull); + assert(row.length == 2); + assert(row[0] == 1); + assert(row[1] == "aa"); + + nrow = cn.queryRow(prepareSQL, 2, "bb"); + assert(!nrow.isNull); + assert(row.length == 2); + assert(row[0] == 2); + assert(row[1] == "bb"); + + nrow = cn.queryRow(prepareSQL, [MySQLVal(3), MySQLVal("cc")]); + assert(!nrow.isNull); + assert(row.length == 2); + assert(row[0] == 3); + assert(row[1] == "cc"); + + // Prepared sql + auto prepared = cn.prepare(prepareSQL); + prepared.setArgs(1, "aa"); + nrow = cn.queryRow(prepared); + assert(!nrow.isNull); + assert(row.length == 2); + assert(row[0] == 1); + assert(row[1] == "aa"); + + nrow = cn.queryRow(prepared, 2, "bb"); + assert(!nrow.isNull); + assert(row.length == 2); + assert(row[0] == 2); + assert(row[1] == "bb"); + + nrow = cn.queryRow(prepared, [MySQLVal(3), MySQLVal("cc")]); + assert(!nrow.isNull); + assert(row.length == 2); + assert(row[0] == 3); + assert(row[1] == "cc"); + + // BCPrepared sql + auto bcPrepared = cn.prepareBackwardCompatImpl(prepareSQL); + bcPrepared.setArgs(1, "aa"); + nrow = cn.queryRow(bcPrepared); + assert(!nrow.isNull); + assert(row.length == 2); + assert(row[0] == 1); + assert(row[1] == "aa"); + } + + // Test queryRowTuple + { + int i; + string s; + + // String sql + cn.queryRowTuple("SELECT * FROM `queryOverloads` WHERE `i`=1 AND `s`=\"aa\"", i, s); + assert(i == 1); + assert(s == "aa"); + + // Prepared sql + auto prepared = cn.prepare(prepareSQL); + prepared.setArgs(2, "bb"); + cn.queryRowTuple(prepared, i, s); + assert(i == 2); + assert(s == "bb"); + + // BCPrepared sql + auto bcPrepared = cn.prepareBackwardCompatImpl(prepareSQL); + bcPrepared.setArgs(3, "cc"); + cn.queryRowTuple(bcPrepared, i, s); + assert(i == 3); + assert(s == "cc"); + } + + // Test queryValue + { + Nullable!MySQLVal value; + + // String sql + value = cn.queryValue("SELECT * FROM `queryOverloads` WHERE `i`=1 AND `s`=\"aa\""); + assert(!value.isNull); + assert(value.get.kind != MySQLVal.Kind.Null); + assert(value.get == 1); + + value = cn.queryValue(prepareSQL, 2, "bb"); + assert(!value.isNull); + assert(value.get.kind != MySQLVal.Kind.Null); + assert(value.get == 2); + + value = cn.queryValue(prepareSQL, [MySQLVal(3), MySQLVal("cc")]); + assert(!value.isNull); + assert(value.get.kind != MySQLVal.Kind.Null); + assert(value.get == 3); + + // Prepared sql + auto prepared = cn.prepare(prepareSQL); + prepared.setArgs(1, "aa"); + value = cn.queryValue(prepared); + assert(!value.isNull); + assert(value.get.kind != MySQLVal.Kind.Null); + assert(value.get == 1); + + value = cn.queryValue(prepared, 2, "bb"); + assert(!value.isNull); + assert(value.get.kind != MySQLVal.Kind.Null); + assert(value.get == 2); + + value = cn.queryValue(prepared, [MySQLVal(3), MySQLVal("cc")]); + assert(!value.isNull); + assert(value.get.kind != MySQLVal.Kind.Null); + assert(value.get == 3); + + // BCPrepared sql + auto bcPrepared = cn.prepareBackwardCompatImpl(prepareSQL); + bcPrepared.setArgs(1, "aa"); + value = cn.queryValue(bcPrepared); + assert(!value.isNull); + assert(value.get.kind != MySQLVal.Kind.Null); + assert(value.get == 1); + } +} diff --git a/source/mysql/test/common.d b/source/mysql/test/common.d index 51f5f694..0b751226 100644 --- a/source/mysql/test/common.d +++ b/source/mysql/test/common.d @@ -17,7 +17,7 @@ import std.string; import std.traits; import std.variant; -import mysql.commands; +import mysql.safe.commands; import mysql.connection; import mysql.exceptions; import mysql.protocol.extra_types; diff --git a/source/mysql/test/integration.d b/source/mysql/test/integration.d index ab741ac3..78cdb776 100644 --- a/source/mysql/test/integration.d +++ b/source/mysql/test/integration.d @@ -13,7 +13,7 @@ import std.traits; import std.typecons; import std.variant; -import mysql.commands; +import mysql.safe.commands; import mysql.connection; import mysql.exceptions; import mysql.metadata; @@ -584,9 +584,9 @@ unittest ~")"); //DataSet ds; - Row[] rs; + SafeRow[] rs; //Table tbl; - Row row; + SafeRow row; Prepared stmt; // Index out of bounds throws @@ -974,7 +974,7 @@ unittest " WHERE CHARACTER_SET_NAME=?"); auto val = "utf8"; stmt.setArg(0, val); - auto row = cn.queryRow(stmt); + auto row = cn.queryRow(stmt).get(SafeRow.init); //assert(row.length == 4); assert(row.length == 4); assert(row[0] == "utf8"); @@ -1005,7 +1005,7 @@ unittest { // Test query - ResultRange rseq = cn.query(selectSQL); + SafeResultRange rseq = cn.query(selectSQL); assert(!rseq.empty); assert(rseq.front.length == 2); assert(rseq.front[0] == 11); @@ -1026,7 +1026,7 @@ unittest { // Test prepared query - ResultRange rseq = cn.query(prepared); + SafeResultRange rseq = cn.query(prepared); assert(!rseq.empty); assert(rseq.front.length == 2); assert(rseq.front[0] == 11); @@ -1047,7 +1047,7 @@ unittest { // Test reusing the same ResultRange - ResultRange rseq = cn.query(selectSQL); + SafeResultRange rseq = cn.query(selectSQL); assert(!rseq.empty); rseq.each(); assert(rseq.empty); @@ -1058,7 +1058,7 @@ unittest } { - Nullable!Row nullableRow; + Nullable!SafeRow nullableRow; // Test queryRow nullableRow = cn.queryRow(selectSQL); @@ -1129,7 +1129,7 @@ unittest { // Issue new command before old command was purged // Ensure old result set is auto-purged and invalidated. - ResultRange rseq1 = cn.query(selectSQL); + SafeResultRange rseq1 = cn.query(selectSQL); rseq1.popFront(); assert(!rseq1.empty); assert(rseq1.isValid); @@ -1142,7 +1142,7 @@ unittest { // Test using outdated ResultRange - ResultRange rseq1 = cn.query(selectSQL); + SafeResultRange rseq1 = cn.query(selectSQL); rseq1.popFront(); assert(!rseq1.empty); assert(rseq1.front[0] == 22); @@ -1154,7 +1154,7 @@ unittest assertThrown!MYXInvalidatedRange(rseq1.popFront()); assertThrown!MYXInvalidatedRange(rseq1.asAA()); - ResultRange rseq2 = cn.query(selectBackwardsSQL); + SafeResultRange rseq2 = cn.query(selectBackwardsSQL); assert(!rseq2.empty); assert(rseq2.front.length == 2); assert(rseq2.front[0] == "ccc"); diff --git a/source/mysql/types.d b/source/mysql/types.d index 95280185..bfc1dacb 100644 --- a/source/mysql/types.d +++ b/source/mysql/types.d @@ -102,10 +102,11 @@ package MySQLVal _toVal(Variant v) import std.meta; import std.traits; import mysql.exceptions; - alias AllTypes = AliasSeq!(bool, byte, ubyte, short, ushort, int, uint, long, ulong, float, double, DateTime, TimeOfDay, Date, string, ubyte[], Timestamp); + alias BasicTypes = AliasSeq!(bool, byte, ubyte, short, ushort, int, uint, long, ulong, float, double, DateTime, TimeOfDay, Date, Timestamp); + alias ArrayTypes = AliasSeq!(char, ubyte); switch (ts) { - static foreach(Type; AllTypes) + static foreach(Type; BasicTypes) { case fullyQualifiedName!Type: case "const(" ~ fullyQualifiedName!Type ~ ")": @@ -116,6 +117,17 @@ package MySQLVal _toVal(Variant v) else return MySQLVal(v.get!(const(Type))); } + static foreach(Type; ArrayTypes) + { + case fullyQualifiedName!Type ~ "[]": + case "const(" ~ fullyQualifiedName!Type ~ ")[]": + case "immutable(" ~ fullyQualifiedName!Type ~ ")[]": + case "shared(immutable(" ~ fullyQualifiedName!Type ~ "))[]": + if(isRef) + return MySQLVal(v.get!(const(Type[]*))); + else + return MySQLVal(v.get!(const(Type[]))); + } default: throw new MYX("Unsupported Database Variant Type: " ~ ts); } @@ -124,13 +136,11 @@ package MySQLVal _toVal(Variant v) /++ Use this as a stop-gap measure in order to keep Variant compatibility. Append this to any function which returns a MySQLVal until you can update your code. +/ -deprecated("Variant support is deprecated. Please switch to using MySQLVal") Variant asVariant(MySQLVal v) { return v.apply!((a) => Variant(a)); } -deprecated("Variant support is deprecated. Please switch to using MySQLVal") Nullable!Variant asVariant(Nullable!MySQLVal v) { if(v.isNull) diff --git a/source/mysql/unsafe/commands.d b/source/mysql/unsafe/commands.d new file mode 100644 index 00000000..615846ca --- /dev/null +++ b/source/mysql/unsafe/commands.d @@ -0,0 +1,825 @@ +/++ +Use a DB via plain SQL statements. + +Commands that are expected to return a result set - queries - have distinctive +methods that are enforced. That is it will be an error to call such a method +with an SQL command that does not produce a result set. So for commands like +SELECT, use the `query` functions. For other commands, like +INSERT/UPDATE/CREATE/etc, use `exec`. ++/ + +module mysql.unsafe.commands; +import SC = mysql.safe.commands; + +import std.conv; +import std.exception; +import std.range; +import std.typecons; +import std.variant; + +import mysql.connection; +import mysql.exceptions; +import mysql.prepared; +import mysql.protocol.comms; +import mysql.protocol.constants; +import mysql.protocol.extra_types; +import mysql.protocol.packets; +import mysql.result; +import mysql.types; + +alias ColumnSpecialization = SC.ColumnSpecialization; +alias CSN = ColumnSpecialization; + +@("columnSpecial") +debug(MYSQLN_TESTS) +unittest +{ + import std.array; + import std.range; + import mysql.test.common; + mixin(scopedCn); + + // Setup + cn.exec("DROP TABLE IF EXISTS `columnSpecial`"); + cn.exec("CREATE TABLE `columnSpecial` ( + `data` LONGBLOB + ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + + immutable totalSize = 1000; // Deliberately not a multiple of chunkSize below + auto alph = cast(const(ubyte)[]) "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + auto data = alph.cycle.take(totalSize).array; + cn.exec("INSERT INTO `columnSpecial` VALUES (\""~(cast(const(char)[])data)~"\")"); + + // Common stuff + int chunkSize; + immutable selectSQL = "SELECT `data` FROM `columnSpecial`"; + ubyte[] received; + bool lastValueOfFinished; + void receiver(const(ubyte)[] chunk, bool finished) @safe + { + assert(lastValueOfFinished == false); + + if(finished) + assert(chunk.length == chunkSize); + else + assert(chunk.length < chunkSize); // Not always true in general, but true in this unittest + + received ~= chunk; + lastValueOfFinished = finished; + } + + // Sanity check + auto value = cn.queryValue(selectSQL); + assert(!value.isNull); + assert(value.get == data); + + // Use ColumnSpecialization with sql string, + // and totalSize as a multiple of chunkSize + { + chunkSize = 100; + assert(cast(int)(totalSize / chunkSize) * chunkSize == totalSize); + auto columnSpecial = ColumnSpecialization(0, 0xfc, chunkSize, &receiver); + + received = null; + lastValueOfFinished = false; + value = cn.queryValue(selectSQL, [columnSpecial]); + assert(!value.isNull); + assert(value.get == data); + //TODO: ColumnSpecialization is not yet implemented + //assert(lastValueOfFinished == true); + //assert(received == data); + } + + // Use ColumnSpecialization with sql string, + // and totalSize as a non-multiple of chunkSize + { + chunkSize = 64; + assert(cast(int)(totalSize / chunkSize) * chunkSize != totalSize); + auto columnSpecial = ColumnSpecialization(0, 0xfc, chunkSize, &receiver); + + received = null; + lastValueOfFinished = false; + value = cn.queryValue(selectSQL, [columnSpecial]); + assert(!value.isNull); + assert(value.get == data); + //TODO: ColumnSpecialization is not yet implemented + //assert(lastValueOfFinished == true); + //assert(received == data); + } + + // Use ColumnSpecialization with prepared statement, + // and totalSize as a multiple of chunkSize + { + chunkSize = 100; + assert(cast(int)(totalSize / chunkSize) * chunkSize == totalSize); + auto columnSpecial = ColumnSpecialization(0, 0xfc, chunkSize, &receiver); + + received = null; + lastValueOfFinished = false; + auto prepared = cn.prepare(selectSQL); + prepared.columnSpecials = [columnSpecial]; + value = cn.queryValue(prepared); + assert(!value.isNull); + assert(value.get == data); + //TODO: ColumnSpecialization is not yet implemented + //assert(lastValueOfFinished == true); + //assert(received == data); + } +} + +/++ +Execute an SQL command or prepared statement, such as INSERT/UPDATE/CREATE/etc. + +This method is intended for commands such as which do not produce a result set +(otherwise, use one of the `query` functions instead.) If the SQL command does +produces a result set (such as SELECT), `mysql.exceptions.MYXResultRecieved` +will be thrown. + +If `args` is supplied, the sql string will automatically be used as a prepared +statement. Prepared statements are automatically cached by mysql-native, +so there's no performance penalty for using this multiple times for the +same statement instead of manually preparing a statement. + +If `args` and `prepared` are both provided, `args` will be used, +and any arguments that are already set in the prepared statement +will automatically be replaced with `args` (note, just like calling +`mysql.prepared.Prepared.setArgs`, this will also remove all +`mysql.prepared.ParameterSpecialization` that may have been applied). + +Only use the `const(char[]) sql` overload that doesn't take `args` +when you are not going to be using the same +command repeatedly and you are CERTAIN all the data you're sending is properly +escaped. Otherwise, consider using overload that takes a `Prepared`. + +If you need to use any `mysql.prepared.ParameterSpecialization`, use +`mysql.connection.prepare` to manually create a `mysql.prepared.Prepared`, +and set your parameter specializations using `mysql.prepared.Prepared.setArg` +or `mysql.prepared.Prepared.setArgs`. + +Type_Mappings: $(TYPE_MAPPINGS) + +Params: +conn = An open `mysql.connection.Connection` to the database. +sql = The SQL command to be run. +prepared = The prepared statement to be run. + +Returns: The number of rows affected. + +Example: +--- +auto myInt = 7; +auto rowsAffected = myConnection.exec("INSERT INTO `myTable` (`a`) VALUES (?)", myInt); +--- ++/ +alias exec = SC.exec; +///ditto +ulong exec(Connection conn, const(char[]) sql, Variant[] args) +{ + auto prepared = conn.prepare(sql); + prepared.setArgs(args); + return exec(conn, prepared); +} +///ditto +ulong exec(Connection conn, ref Prepared prepared, Variant[] args) +{ + prepared.setArgs(args); + return exec(conn, prepared); +} + +/++ +Execute an SQL SELECT command or prepared statement. + +This returns an input range of `mysql.result.UnsafeRow`, so if you need random access +to the `mysql.result.UnsafeRow` elements, simply call +$(LINK2 https://dlang.org/phobos/std_array.html#array, `std.array.array()`) +on the result. + +If the SQL command does not produce a result set (such as INSERT/CREATE/etc), +then `mysql.exceptions.MYXNoResultRecieved` will be thrown. Use +`exec` instead for such commands. + +If `args` is supplied, the sql string will automatically be used as a prepared +statement. Prepared statements are automatically cached by mysql-native, +so there's no performance penalty for using this multiple times for the +same statement instead of manually preparing a statement. + +If `args` and `prepared` are both provided, `args` will be used, +and any arguments that are already set in the prepared statement +will automatically be replaced with `args` (note, just like calling +`mysql.prepared.Prepared.setArgs`, this will also remove all +`mysql.prepared.ParameterSpecialization` that may have been applied). + +Only use the `const(char[]) sql` overload that doesn't take `args` +when you are not going to be using the same +command repeatedly and you are CERTAIN all the data you're sending is properly +escaped. Otherwise, consider using overload that takes a `Prepared`. + +If you need to use any `mysql.prepared.ParameterSpecialization`, use +`mysql.connection.prepare` to manually create a `mysql.prepared.Prepared`, +and set your parameter specializations using `mysql.prepared.Prepared.setArg` +or `mysql.prepared.Prepared.setArgs`. + +Type_Mappings: $(TYPE_MAPPINGS) + +Params: +conn = An open `mysql.connection.Connection` to the database. +sql = The SQL command to be run. +prepared = The prepared statement to be run. +csa = Not yet implemented. + +Returns: A (possibly empty) `mysql.result.UnsafeResultRange`. + +Example: +--- +UnsafeResultRange oneAtATime = myConnection.query("SELECT * from `myTable`"); +UnsafeRow[] allAtOnce = myConnection.query("SELECT * from `myTable`").array; + +auto myInt = 7; +UnsafeResultRange rows = myConnection.query("SELECT * FROM `myTable` WHERE `a` = ?", myInt); +--- ++/ +/+ +Future text: +If there are long data items among the expected result columns you can use +the `csa` param to specify that they are to be subject to chunked transfer via a +delegate. + +csa = An optional array of `ColumnSpecialization` structs. If you need to +use this with a prepared statement, please use `mysql.prepared.Prepared.columnSpecials`. ++/ +UnsafeResultRange query(Connection conn, const(char[]) sql, ColumnSpecialization[] csa = null) @safe +{ + return SC.query(conn, sql, csa).unsafe; +} +///ditto +UnsafeResultRange query(T...)(Connection conn, const(char[]) sql, T args) + if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) +{ + return SC.query(conn, sql, args).unsafe; +} +///ditto +UnsafeResultRange query(Connection conn, const(char[]) sql, Variant[] args) +{ + auto prepared = conn.prepare(sql); + prepared.setArgs(args); + return query(conn, prepared); +} +///ditto +UnsafeResultRange query(Connection conn, ref Prepared prepared) +{ + return SC.query(conn, prepared).unsafe; +} +///ditto +UnsafeResultRange query(T...)(Connection conn, ref Prepared prepared, T args) + if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) +{ + return SC.query(conn, prepared, args).unsafe; +} +///ditto +UnsafeResultRange query(Connection conn, ref Prepared prepared, Variant[] args) +{ + prepared.setArgs(args); + return query(conn, prepared); +} + +///ditto +UnsafeResultRange query(Connection conn, ref BackwardCompatPrepared prepared) +{ + auto p = prepared.prepared; + auto result = query(conn, p); + prepared._prepared = p; + return result; +} + +/++ +Execute an SQL SELECT command or prepared statement where you only want the +first `mysql.result.UnsafeRow`, if any. + +If the SQL command does not produce a result set (such as INSERT/CREATE/etc), +then `mysql.exceptions.MYXNoResultRecieved` will be thrown. Use +`exec` instead for such commands. + +If `args` is supplied, the sql string will automatically be used as a prepared +statement. Prepared statements are automatically cached by mysql-native, +so there's no performance penalty for using this multiple times for the +same statement instead of manually preparing a statement. + +If `args` and `prepared` are both provided, `args` will be used, +and any arguments that are already set in the prepared statement +will automatically be replaced with `args` (note, just like calling +`mysql.prepared.Prepared.setArgs`, this will also remove all +`mysql.prepared.ParameterSpecialization` that may have been applied). + +Only use the `const(char[]) sql` overload that doesn't take `args` +when you are not going to be using the same +command repeatedly and you are CERTAIN all the data you're sending is properly +escaped. Otherwise, consider using overload that takes a `Prepared`. + +If you need to use any `mysql.prepared.ParameterSpecialization`, use +`mysql.connection.prepare` to manually create a `mysql.prepared.Prepared`, +and set your parameter specializations using `mysql.prepared.Prepared.setArg` +or `mysql.prepared.Prepared.setArgs`. + +Type_Mappings: $(TYPE_MAPPINGS) + +Params: +conn = An open `mysql.connection.Connection` to the database. +sql = The SQL command to be run. +prepared = The prepared statement to be run. +csa = Not yet implemented. + +Returns: `Nullable!(mysql.result.UnsafeRow)`: This will be null (check via `Nullable.isNull`) if the +query resulted in an empty result set. + +Example: +--- +auto myInt = 7; +Nullable!UnsafeRow row = myConnection.queryRow("SELECT * FROM `myTable` WHERE `a` = ?", myInt); +--- ++/ +/+ +Future text: +If there are long data items among the expected result columns you can use +the `csa` param to specify that they are to be subject to chunked transfer via a +delegate. + +csa = An optional array of `ColumnSpecialization` structs. If you need to +use this with a prepared statement, please use `mysql.prepared.Prepared.columnSpecials`. ++/ +/+ +Future text: +If there are long data items among the expected result columns you can use +the `csa` param to specify that they are to be subject to chunked transfer via a +delegate. + +csa = An optional array of `ColumnSpecialization` structs. If you need to +use this with a prepared statement, please use `mysql.prepared.Prepared.columnSpecials`. ++/ +Nullable!UnsafeRow queryRow(Connection conn, const(char[]) sql, ColumnSpecialization[] csa = null) +{ + return SC.queryRow(conn, sql, csa).unsafe; +} +///ditto +Nullable!UnsafeRow queryRow(T...)(Connection conn, const(char[]) sql, T args) + if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) +{ + return SC.queryRow(conn, sql, args).unsafe; +} +///ditto +Nullable!UnsafeRow queryRow(Connection conn, const(char[]) sql, Variant[] args) @system +{ + auto prepared = conn.prepare(sql); + prepared.setArgs(args); + return queryRow(conn, prepared); +} +///ditto +Nullable!UnsafeRow queryRow(Connection conn, ref Prepared prepared) +{ + return SC.queryRow(conn, prepared).unsafe; +} +///ditto +Nullable!UnsafeRow queryRow(T...)(Connection conn, ref Prepared prepared, T args) + if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) +{ + return SC.queryRow(conn, prepared, args).unsafe; +} +///ditto +Nullable!UnsafeRow queryRow(Connection conn, ref Prepared prepared, Variant[] args) @system +{ + prepared.setArgs(args); + return queryRow(conn, prepared); +} + +///ditto +Nullable!UnsafeRow queryRow(Connection conn, ref BackwardCompatPrepared prepared) +{ + auto p = prepared.prepared; + auto result = queryRow(conn, p); + prepared._prepared = p; + return result; +} + +/++ +Execute an SQL SELECT command or prepared statement where you only want the +first `mysql.result.UnsafeRow`, and place result values into a set of D variables. + +This method will throw if any column type is incompatible with the corresponding D variable. + +Unlike the other query functions, queryRowTuple will throw +`mysql.exceptions.MYX` if the result set is empty +(and thus the reference variables passed in cannot be filled). + +If the SQL command does not produce a result set (such as INSERT/CREATE/etc), +then `mysql.exceptions.MYXNoResultRecieved` will be thrown. Use +`exec` instead for such commands. + +Only use the `const(char[]) sql` overload when you are not going to be using the same +command repeatedly and you are CERTAIN all the data you're sending is properly +escaped. Otherwise, consider using overload that takes a `Prepared`. + +Type_Mappings: $(TYPE_MAPPINGS) + +Params: +conn = An open `mysql.connection.Connection` to the database. +sql = The SQL command to be run. +prepared = The prepared statement to be run. +args = The variables, taken by reference, to receive the values. ++/ +alias queryRowTuple = SC.queryRowTuple; +///ditto +void queryRowTuple(T...)(Connection conn, ref BackwardCompatPrepared prepared, ref T args) +{ + auto p = prepared.prepared; + .queryRowTuple(conn, p, args); + prepared._prepared = p; +} + + +/++ +Execute an SQL SELECT command or prepared statement and return a single value: +the first column of the first row received. + +If the query did not produce any rows, or the rows it produced have zero columns, +this will return `Nullable!Variant()`, ie, null. Test for this with `result.isNull`. + +If the query DID produce a result, but the value actually received is NULL, +then `result.isNull` will be FALSE, and `result.get` will produce a Variant +which CONTAINS null. Check for this with `result.get.type == typeid(typeof(null))`. + +If the SQL command does not produce a result set (such as INSERT/CREATE/etc), +then `mysql.exceptions.MYXNoResultRecieved` will be thrown. Use +`exec` instead for such commands. + +If `args` is supplied, the sql string will automatically be used as a prepared +statement. Prepared statements are automatically cached by mysql-native, +so there's no performance penalty for using this multiple times for the +same statement instead of manually preparing a statement. + +If `args` and `prepared` are both provided, `args` will be used, +and any arguments that are already set in the prepared statement +will automatically be replaced with `args` (note, just like calling +`mysql.prepared.Prepared.setArgs`, this will also remove all +`mysql.prepared.ParameterSpecialization` that may have been applied). + +Only use the `const(char[]) sql` overload that doesn't take `args` +when you are not going to be using the same +command repeatedly and you are CERTAIN all the data you're sending is properly +escaped. Otherwise, consider using overload that takes a `Prepared`. + +If you need to use any `mysql.prepared.ParameterSpecialization`, use +`mysql.connection.prepare` to manually create a `mysql.prepared.Prepared`, +and set your parameter specializations using `mysql.prepared.Prepared.setArg` +or `mysql.prepared.Prepared.setArgs`. + +Type_Mappings: $(TYPE_MAPPINGS) + +Params: +conn = An open `mysql.connection.Connection` to the database. +sql = The SQL command to be run. +prepared = The prepared statement to be run. +csa = Not yet implemented. + +Returns: `Nullable!Variant`: This will be null (check via `Nullable.isNull`) if the +query resulted in an empty result set. + +Example: +--- +auto myInt = 7; +Nullable!Variant value = myConnection.queryRow("SELECT * FROM `myTable` WHERE `a` = ?", myInt); +--- ++/ +/+ +Future text: +If there are long data items among the expected result columns you can use +the `csa` param to specify that they are to be subject to chunked transfer via a +delegate. + +csa = An optional array of `ColumnSpecialization` structs. If you need to +use this with a prepared statement, please use `mysql.prepared.Prepared.columnSpecials`. ++/ +/+ +Future text: +If there are long data items among the expected result columns you can use +the `csa` param to specify that they are to be subject to chunked transfer via a +delegate. + +csa = An optional array of `ColumnSpecialization` structs. If you need to +use this with a prepared statement, please use `mysql.prepared.Prepared.columnSpecials`. ++/ +Nullable!Variant queryValue(Connection conn, const(char[]) sql, ColumnSpecialization[] csa = null) +{ + return SC.queryValue(conn, sql, csa).asVariant; +} +///ditto +Nullable!Variant queryValue(T...)(Connection conn, const(char[]) sql, T args) + if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) +{ + return SC.queryValue(conn, sql, args).asVariant; +} +///ditto +Nullable!Variant queryValue(Connection conn, const(char[]) sql, Variant[] args) @system +{ + auto prepared = conn.prepare(sql); + prepared.setArgs(args); + return queryValue(conn, prepared); +} +///ditto +Nullable!Variant queryValue(Connection conn, ref Prepared prepared) +{ + return SC.queryValue(conn, prepared).asVariant; +} +///ditto +Nullable!Variant queryValue(T...)(Connection conn, ref Prepared prepared, T args) + if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) +{ + return SC.queryValue(conn, prepared, args).asVariant; +} +///ditto +Nullable!Variant queryValue(Connection conn, ref Prepared prepared, Variant[] args) @system +{ + prepared.setArgs(args); + return queryValue(conn, prepared); +} +///ditto +Nullable!Variant queryValue(Connection conn, ref BackwardCompatPrepared prepared) +{ + auto p = prepared.prepared; + auto result = queryValue(conn, p); + prepared._prepared = p; + return result; +} + + +@("execOverloads") +debug(MYSQLN_TESTS) +unittest +{ + import std.array; + import mysql.connection; + import mysql.test.common; + mixin(scopedCn); + + cn.exec("DROP TABLE IF EXISTS `execOverloads`"); + cn.exec("CREATE TABLE `execOverloads` ( + `i` INTEGER, + `s` VARCHAR(50) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + + immutable prepareSQL = "INSERT INTO `execOverloads` VALUES (?, ?)"; + + // Do the inserts, using exec + + // exec: const(char[]) sql + assert(cn.exec("INSERT INTO `execOverloads` VALUES (1, \"aa\")") == 1); + assert(cn.exec(prepareSQL, 2, "bb") == 1); + assert(cn.exec(prepareSQL, [Variant(3), Variant("cc")]) == 1); + + // exec: prepared sql + auto prepared = cn.prepare(prepareSQL); + prepared.setArgs(4, "dd"); + assert(cn.exec(prepared) == 1); + + assert(cn.exec(prepared, 5, "ee") == 1); + assert(prepared.getArg(0) == 5); + assert(prepared.getArg(1) == "ee"); + + assert(cn.exec(prepared, [Variant(6), Variant("ff")]) == 1); + assert(prepared.getArg(0) == 6); + assert(prepared.getArg(1) == "ff"); + + // exec: bcPrepared sql + auto bcPrepared = cn.prepareBackwardCompatImpl(prepareSQL); + bcPrepared.setArgs(7, "gg"); + assert(cn.exec(bcPrepared) == 1); + assert(bcPrepared.getArg(0) == 7); + assert(bcPrepared.getArg(1) == "gg"); + + // Check results + auto rows = cn.query("SELECT * FROM `execOverloads`").array(); + assert(rows.length == 7); + + assert(rows[0].length == 2); + assert(rows[1].length == 2); + assert(rows[2].length == 2); + assert(rows[3].length == 2); + assert(rows[4].length == 2); + assert(rows[5].length == 2); + assert(rows[6].length == 2); + + assert(rows[0][0] == 1); + assert(rows[0][1] == "aa"); + assert(rows[1][0] == 2); + assert(rows[1][1] == "bb"); + assert(rows[2][0] == 3); + assert(rows[2][1] == "cc"); + assert(rows[3][0] == 4); + assert(rows[3][1] == "dd"); + assert(rows[4][0] == 5); + assert(rows[4][1] == "ee"); + assert(rows[5][0] == 6); + assert(rows[5][1] == "ff"); + assert(rows[6][0] == 7); + assert(rows[6][1] == "gg"); +} + +@("queryOverloads") +debug(MYSQLN_TESTS) +unittest +{ + import std.array; + import mysql.connection; + import mysql.test.common; + mixin(scopedCn); + + cn.exec("DROP TABLE IF EXISTS `queryOverloads`"); + cn.exec("CREATE TABLE `queryOverloads` ( + `i` INTEGER, + `s` VARCHAR(50) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + cn.exec("INSERT INTO `queryOverloads` VALUES (1, \"aa\"), (2, \"bb\"), (3, \"cc\")"); + + immutable prepareSQL = "SELECT * FROM `queryOverloads` WHERE `i`=? AND `s`=?"; + + // Test query + { + UnsafeRow[] rows; + + // String sql + rows = cn.query("SELECT * FROM `queryOverloads` WHERE `i`=1 AND `s`=\"aa\"").array; + assert(rows.length == 1); + assert(rows[0].length == 2); + assert(rows[0][0] == 1); + assert(rows[0][1] == "aa"); + + rows = cn.query(prepareSQL, 2, "bb").array; + assert(rows.length == 1); + assert(rows[0].length == 2); + assert(rows[0][0] == 2); + assert(rows[0][1] == "bb"); + + rows = cn.query(prepareSQL, [Variant(3), Variant("cc")]).array; + assert(rows.length == 1); + assert(rows[0].length == 2); + assert(rows[0][0] == 3); + assert(rows[0][1] == "cc"); + + // Prepared sql + auto prepared = cn.prepare(prepareSQL); + prepared.setArgs(1, "aa"); + rows = cn.query(prepared).array; + assert(rows.length == 1); + assert(rows[0].length == 2); + assert(rows[0][0] == 1); + assert(rows[0][1] == "aa"); + + rows = cn.query(prepared, 2, "bb").array; + assert(rows.length == 1); + assert(rows[0].length == 2); + assert(rows[0][0] == 2); + assert(rows[0][1] == "bb"); + + rows = cn.query(prepared, [Variant(3), Variant("cc")]).array; + assert(rows.length == 1); + assert(rows[0].length == 2); + assert(rows[0][0] == 3); + assert(rows[0][1] == "cc"); + + // BCPrepared sql + auto bcPrepared = cn.prepareBackwardCompatImpl(prepareSQL); + bcPrepared.setArgs(1, "aa"); + rows = cn.query(bcPrepared).array; + assert(rows.length == 1); + assert(rows[0].length == 2); + assert(rows[0][0] == 1); + assert(rows[0][1] == "aa"); + } + + // Test queryRow + { + Nullable!UnsafeRow nrow; + // avoid always saying nrow.get + UnsafeRow row() { return nrow.get; } + + // String sql + nrow = cn.queryRow("SELECT * FROM `queryOverloads` WHERE `i`=1 AND `s`=\"aa\""); + assert(!nrow.isNull); + assert(row.length == 2); + assert(row[0] == 1); + assert(row[1] == "aa"); + + nrow = cn.queryRow(prepareSQL, 2, "bb"); + assert(!nrow.isNull); + assert(row.length == 2); + assert(row[0] == 2); + assert(row[1] == "bb"); + + nrow = cn.queryRow(prepareSQL, [Variant(3), Variant("cc")]); + assert(!nrow.isNull); + assert(row.length == 2); + assert(row[0] == 3); + assert(row[1] == "cc"); + + // Prepared sql + auto prepared = cn.prepare(prepareSQL); + prepared.setArgs(1, "aa"); + nrow = cn.queryRow(prepared); + assert(!nrow.isNull); + assert(row.length == 2); + assert(row[0] == 1); + assert(row[1] == "aa"); + + nrow = cn.queryRow(prepared, 2, "bb"); + assert(!nrow.isNull); + assert(row.length == 2); + assert(row[0] == 2); + assert(row[1] == "bb"); + + nrow = cn.queryRow(prepared, [Variant(3), Variant("cc")]); + assert(!nrow.isNull); + assert(row.length == 2); + assert(row[0] == 3); + assert(row[1] == "cc"); + + // BCPrepared sql + auto bcPrepared = cn.prepareBackwardCompatImpl(prepareSQL); + bcPrepared.setArgs(1, "aa"); + nrow = cn.queryRow(bcPrepared); + assert(!nrow.isNull); + assert(row.length == 2); + assert(row[0] == 1); + assert(row[1] == "aa"); + } + + // Test queryRowTuple + { + int i; + string s; + + // String sql + cn.queryRowTuple("SELECT * FROM `queryOverloads` WHERE `i`=1 AND `s`=\"aa\"", i, s); + assert(i == 1); + assert(s == "aa"); + + // Prepared sql + auto prepared = cn.prepare(prepareSQL); + prepared.setArgs(2, "bb"); + cn.queryRowTuple(prepared, i, s); + assert(i == 2); + assert(s == "bb"); + + // BCPrepared sql + auto bcPrepared = cn.prepareBackwardCompatImpl(prepareSQL); + bcPrepared.setArgs(3, "cc"); + cn.queryRowTuple(bcPrepared, i, s); + assert(i == 3); + assert(s == "cc"); + } + + // Test queryValue + { + Nullable!Variant value; + + // String sql + value = cn.queryValue("SELECT * FROM `queryOverloads` WHERE `i`=1 AND `s`=\"aa\""); + auto nullType = typeid(typeof(null)); + assert(!value.isNull); + assert(value.get.type != nullType); + assert(value.get == 1); + + value = cn.queryValue(prepareSQL, 2, "bb"); + assert(!value.isNull); + assert(value.get.type != nullType); + assert(value.get == 2); + + value = cn.queryValue(prepareSQL, [Variant(3), Variant("cc")]); + assert(!value.isNull); + assert(value.get.type != nullType); + assert(value.get == 3); + + // Prepared sql + auto prepared = cn.prepare(prepareSQL); + prepared.setArgs(1, "aa"); + value = cn.queryValue(prepared); + assert(!value.isNull); + assert(value.get.type != nullType); + assert(value.get == 1); + + value = cn.queryValue(prepared, 2, "bb"); + assert(!value.isNull); + assert(value.get.type != nullType); + assert(value.get == 2); + + value = cn.queryValue(prepared, [Variant(3), Variant("cc")]); + assert(!value.isNull); + assert(value.get.type != nullType); + assert(value.get == 3); + + // BCPrepared sql + auto bcPrepared = cn.prepareBackwardCompatImpl(prepareSQL); + bcPrepared.setArgs(1, "aa"); + value = cn.queryValue(bcPrepared); + assert(!value.isNull); + assert(value.get.type != nullType); + assert(value.get == 1); + } +} From 7e62f3f63e5a118e5bcb1ada2f61a298dad7136b Mon Sep 17 00:00:00 2001 From: Steven Schveighoffer Date: Thu, 6 Feb 2020 21:38:05 -0500 Subject: [PATCH 10/14] This is a big one. All the API is now funnelled through safe and unsafe packages. Everything should be 100% backwards compatible --- .gitignore | 1 + SAFE_MIGRATION.md | 88 ++ dub.selections.json | 7 +- examples/homePage/example.d | 4 +- source/mysql/commands.d | 16 +- source/mysql/connection.d | 1446 +-------------------------- source/mysql/exceptions.d | 2 +- source/mysql/internal/connection.d | 1218 ++++++++++++++++++++++ source/mysql/internal/pool.d | 629 ++++++++++++ source/mysql/internal/prepared.d | 819 +++++++++++++++ source/mysql/internal/result.d | 391 ++++++++ source/mysql/metadata.d | 5 +- source/mysql/package.d | 24 +- source/mysql/pool.d | 595 +---------- source/mysql/prepared.d | 761 +------------- source/mysql/protocol/comms.d | 2 +- source/mysql/protocol/extra_types.d | 4 +- source/mysql/protocol/packets.d | 2 +- source/mysql/result.d | 401 +------- source/mysql/safe/commands.d | 78 +- source/mysql/safe/connection.d | 125 +++ source/mysql/safe/package.d | 20 + source/mysql/safe/pool.d | 6 + source/mysql/safe/prepared.d | 6 + source/mysql/safe/result.d | 6 + source/mysql/test/common.d | 2 +- source/mysql/test/integration.d | 34 +- source/mysql/test/regression.d | 8 +- source/mysql/unsafe/commands.d | 26 +- source/mysql/unsafe/connection.d | 147 +++ source/mysql/unsafe/package.d | 20 + source/mysql/unsafe/pool.d | 6 + source/mysql/unsafe/prepared.d | 6 + source/mysql/unsafe/result.d | 6 + 34 files changed, 3576 insertions(+), 3335 deletions(-) create mode 100644 SAFE_MIGRATION.md create mode 100644 source/mysql/internal/connection.d create mode 100644 source/mysql/internal/pool.d create mode 100644 source/mysql/internal/prepared.d create mode 100644 source/mysql/internal/result.d create mode 100644 source/mysql/safe/connection.d create mode 100644 source/mysql/safe/package.d create mode 100644 source/mysql/safe/pool.d create mode 100644 source/mysql/safe/prepared.d create mode 100644 source/mysql/safe/result.d create mode 100644 source/mysql/unsafe/connection.d create mode 100644 source/mysql/unsafe/package.d create mode 100644 source/mysql/unsafe/pool.d create mode 100644 source/mysql/unsafe/prepared.d create mode 100644 source/mysql/unsafe/result.d diff --git a/.gitignore b/.gitignore index 4005ad83..9c6a5f88 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ *.dll *.exe .dub +.*.swp /bin /testConnectionStr.txt diff --git a/SAFE_MIGRATION.md b/SAFE_MIGRATION.md new file mode 100644 index 00000000..cba721e5 --- /dev/null +++ b/SAFE_MIGRATION.md @@ -0,0 +1,88 @@ +# Migrating code to use the @safe API of mysql-native + +This document describes how mysql-native is migrating to an all-@safe API and library, and how you can migrate your existing code to the new version. + +First, note that the latest version of mysql, while it supports a safe API, is defaulted to supporting the original unsafe API. We highly recommend reading and following the recommendations in this document so you can start using the safe version. + +## Why Safe? + +*related: please see D's [Memory Safety](https://dlang.org/spec/memory-safe-d.html) page to understand what `@safe` does in D* + +Since mysql-native is intended to be a key component of servers that are on the Internet, it must support the capability (even if not required) to be fully `@safe`. In addition, major web frameworks (e.g. [vibe.d](http://code.dlang.org/packages/vibe-d)) and arguably any other program is headed in this direction. + +In other words, the world wants memory safe code, and libraries that provide safe interfaces and guarantees will be much more appealing. It's just not acceptable any more for the components of major development projects to be careless about memory safety. + +## The Major Changes + +mysql-native until now used the Phobos type `std.variant.Variant` to hold data who's type was unknown at compile time. Unfortunately, since `Variant` can hold *any* type, it must default to having a `@system` postblit/copy constructor, and a `@system` destructor. This means that just copying a `Variant`, passing it as a function parameter, or returning it from a function makes the function doing such things `@system`. This meant that we needed to move from `Variant` to a new type that allows only safe usages, but still maintained the ability to decide types at runtime. + +To this end, we used the library [taggedalgebraic](http://code.dlang.org/packages/taggedalgebraic), which supports not only safe call forwarding, but also provides a much more transparent and useful API than Variant. A `TaggedAlgebraic` allows you to limit which types MySQL deals with. This allows better implicit conversion support, and more focused code. It also prevents one from passing in parameter types that are not supported and not finding that out until runtime. + +The module `mysql.types` now contains a new type called `MySQLVal`, which should be, for the most part, a drop-in replacement for `Variant` in your code. + +### The safe/unsafe API + +In some cases, fixing memory safety in mysql-native was as simple as adding a `@safe` tag to the module or functions in the module. These functions should work just as before, but are now callable from `@safe` code. + +But for the rest, to achieve full backwards compatibility, we have divided the API into two major sections -- safe and unsafe. The package `mysql.safe` will import all the safe versions of the API, the package `mysql.unsafe` will import the unsafe versions. If you import `mysql`, it will currently point at the unsafe version for backwards compatibility. + +The following modules have been split into mysql.safe.*modname* and mysql.unsafe.*modname*. Importing mysql.*modname* will import the unsafe version for backwards compatibility. +* module mysql.commands +* module mysql.pool +* module mysql.result +* module mysql.prepared +* module mysql.connection + +Each of these modules in unsafe mode provides the same API as the previous version of mysql. The safe version provides aliases to the original type names for the safe versions of types, and also provides the same functions as before that can be called via safe code. The one exception is in `mysql.safe.commands`, where some functions were for the deprecated `BackwardCompatPrepared`, which will eventually be removed. + +If you are currently importing any of the above modules directly, or importing the `mysql` package, a first step to migration is to use the `mysql.safe` package. From there you will find that almost everything works exactly the same. + +### Migrating from Variant to MySQLVal + +The module `mysql.types` has been amended to contain the `MySQLVal` type. This type can hold any value type that MySQL supported originally, or a const pointer to such a type (for the purposes of prepared statements), or the value `null`. This is now the type used for all parameters to `query` and `exec` (in the safe API). The `mysql.types` import also provides compatibility shims with `Variant` such as `coerce`, `convertsTo`, `type`, `peek`, and `get` (See the documentation for [Variant](https://dlang.org/phobos/std_variant.html#.VariantN)). + +You can examine all the benefits of `TaggedAlgebraic` [here](https://vibed.org/api/taggedalgebraic.taggedalgebraic/TaggedAlgebraic). In particular, the usage of the `kind` member is preferred over using the `type` shim. Note that only safe operations are allowed, so for instance `opBinary!"+"` is not allowed on pointers. + +One pitfall of this migration has to do with `Variant`'s ability to represent *any* type -- including `MySQLVal`! If you have declared a variable of type `Variant`, and assign it to a `MySQLVal` result from a row or a query, it will compile, but it will NOT do what you are expecting. This will fail at runtime most likely. It is recommended before switching to the safe API to change those types to `MySQLVal` or use `auto` if possible. + +The `mysql.types` module also contains a compatibility function `asVariant`, which can be used when you want to use the safe API but absolutely need a `Variant` from a `MySQLVal`. The opposite conversion is implemented, but not exposed publically since there is no compatibility issue for existing code. + +One important thing to note is that the internals of mysql-native have all been switched to using `MySQLVal` instead of `Variant`. Only at the shallow API level is `Variant` used to provide the backwards compatible API. So if you do not switch, you will pay the penalty of having the library first construct a `MySQLVal` and then convert that to a `Variant` (or vice versa). + +### Row and ResultRange + +These two types were tied greatly to `Variant`. As such, they have been rewritten into `SafeRow` and `SafeResultRange` which use `MySQLVal` instead. Thin compatibility wrappers of `UnsafeRow` and `UnsafeResultRange` are available as well, which will convert the values to and from `Variant` as needed. Depending on which API you import `safe` or `unsafe`, these items are aliased to `Row` and `ResultRange` for source compatibility. + +For this reason, you should not import both the `safe` and `unsafe` API, as you will get ambiguity errors. + +However, each of these structures provides `unsafe` and `safe` conversion functions to convert between the two if absolutely necessary. In fact, most of the unsafe API calls that return an `UnsafeRow` or `UnsafeResultRange` are actually `@safe`, since the underlying implementation uses `MySQLVal`. It only becomes unsafe when you try to access a column as a `Variant`. + +TODO: some examples needed + +### Prepared + +The `Prepared` struct contained support for setting/getting `Variant` parameters. These have been removed, and reimplemented as a `SafePrepared` struct, which uses `MySQLVal` instead. An `UnsafePrepared` wrapper has been provided, and like `Row`/`ResultSequence`, they have `unsafe`, and `safe` conversion functions. + +The `mysql.safe.prepared` module will alias `Prepared` as the safe version, and the `mysql.unsafe.prepared` module will alias `Prepared` as the unsafe version. + +### Connection + +The Connection class itself has not changed at all, except to add @safe for everything. However, the `mysql.connection` module contained the functions to generate `Prepared` structs. + +The `BackwardsCompatPrepared` struct defined in the original `mysql.connection` module is only available in the unsafe package. + +### MySQLPool + +`MySQLPool` has been factored into a templated type that has either a fully safe or partly safe API. The only public facing unsafe part was the user-supplied callback function to be called on every connection creation (which therefore makes `lockConnection` unsafe). The unsafe version continues to use such a callback method (and is explicitly marked `@system`), whereas the safe version requires a `@safe` callback. If you do not use this callback mechanism, it is highly recommended that you use the safe API for the pool, as there is no actual difference between the two at that point. It's also very likely that your callback actually is `@safe`, even if you do use one. + +### The commands module + +As previously mentioned, the `mysql.commands` module has been factored into 2 versions, a safe and unsafe version. The only differences between these two are where `Variant` is concerned. All query and exec functions that accepted `Variant` explicitly have been reimplemented in the safe version to accept `MySQLVal`. All functions that returned `Variant` have been reimplemented to return `MySQLVal`. All functions that do not deal with `Variant` are moved to the safe API, and aliased in the unsafe API. This means, as long as you do not use `Variant` explicitly, you should be able to switch over to the safe version of the API without changing your code. + +TODO: some examples needed + +## Future versions + +The next major version of mysql-native will swap the default package imports to the safe API. In addition, all unsafe functions and types will be marked deprecated. + +In a future major version (not necessarily the one after the above version), the unsafe API will be completely removed, and the safe API will take the place of the default modules. The explicit `mysql.safe` packages will remain for backwards compatibility. At this time, all uses of `Variant` will be gone. diff --git a/dub.selections.json b/dub.selections.json index b2dd43e4..6d5833b0 100644 --- a/dub.selections.json +++ b/dub.selections.json @@ -2,11 +2,8 @@ "fileVersion": 1, "versions": { "eventcore": "0.8.48", - "libasync": "0.8.4", - "libev": "5.0.0+4.04", - "libevent": "2.0.1+2.0.16", - "memutils": "0.4.13", - "openssl": "1.1.4+1.0.1g", + "libasync": "0.8.5", + "memutils": "1.0.4", "stdx-allocator": "2.77.5", "taggedalgebraic": "0.11.8", "unit-threaded": "0.7.55", diff --git a/examples/homePage/example.d b/examples/homePage/example.d index 23f2ee7e..e64a523a 100644 --- a/examples/homePage/example.d +++ b/examples/homePage/example.d @@ -18,8 +18,8 @@ void main(string[] args) // Query ResultRange range = conn.query("SELECT * FROM `tablename`"); Row row = range.front; - auto id = row[0]; - auto name = row[1]; + Variant id = row[0]; + Variant name = row[1]; assert(id == 1); assert(name == "Ann"); diff --git a/source/mysql/commands.d b/source/mysql/commands.d index f8f456f4..4bbb3dd4 100644 --- a/source/mysql/commands.d +++ b/source/mysql/commands.d @@ -1,5 +1,13 @@ +/++ +This module imports `mysql.unsafe.commands`, which provides the Variant-based +interface to mysql. In the future, this will switch to importing the +`mysql.safe.commands`, which provides the @safe interface to mysql. Please see +those two modules for documentation on the functions provided. It is highly +recommended to import `mysql.safe.commands` and not the unsafe commands, as +that is the future for mysql-native. + +In the far future, the unsafe version will be deprecated and removed, and the +safe version moved to this location. ++/ module mysql.commands; -version(MySQLSafeMode) - public import mysql.safe.commands; -else - public import mysql.unsafe.commands; +public import mysql.unsafe.commands; diff --git a/source/mysql/connection.d b/source/mysql/connection.d index 1d0e1e4b..7452813c 100644 --- a/source/mysql/connection.d +++ b/source/mysql/connection.d @@ -1,1447 +1,3 @@ -/// Connect to a MySQL/MariaDB server. module mysql.connection; -import std.algorithm; -import std.conv; -import std.exception; -import std.range; -import std.socket; -import std.string; -import std.typecons; - -import mysql.safe.commands; -import mysql.exceptions; -import mysql.prepared; -import mysql.protocol.comms; -import mysql.protocol.constants; -import mysql.protocol.packets; -import mysql.protocol.sockets; -import mysql.result; -import mysql.types; - -@safe: - -debug(MYSQLN_TESTS) -{ - import mysql.test.common; -} - -version(Have_vibe_core) -{ - static if(__traits(compiles, (){ import vibe.core.net; } )) - import vibe.core.net; - else - static assert(false, "mysql-native can't find Vibe.d's 'vibe.core.net'."); -} - -/// The default `mysql.protocol.constants.SvrCapFlags` used when creating a connection. -immutable SvrCapFlags defaultClientFlags = - SvrCapFlags.OLD_LONG_PASSWORD | SvrCapFlags.ALL_COLUMN_FLAGS | - SvrCapFlags.WITH_DB | SvrCapFlags.PROTOCOL41 | - SvrCapFlags.SECURE_CONNECTION;// | SvrCapFlags.MULTI_STATEMENTS | - //SvrCapFlags.MULTI_RESULTS; - -/++ -Submit an SQL command to the server to be compiled into a prepared statement. - -This will automatically register the prepared statement on the provided connection. -The resulting `mysql.prepared.Prepared` can then be used freely on ANY `Connection`, -as it will automatically be registered upon its first use on other connections. -Or, pass it to `Connection.register` if you prefer eager registration. - -Internally, the result of a successful outcome will be a statement handle - an ID - -for the prepared statement, a count of the parameters required for -execution of the statement, and a count of the columns that will be present -in any result set that the command generates. - -The server will then proceed to send prepared statement headers, -including parameter descriptions, and result set field descriptions, -followed by an EOF packet. - -Throws: `mysql.exceptions.MYX` if the server has a problem. -+/ -Prepared prepare(Connection conn, const(char[]) sql) -{ - auto info = conn.registerIfNeeded(sql); - return Prepared(sql, info.headers, info.numParams); -} - -/++ -This function is provided ONLY as a temporary aid in upgrading to mysql-native v2.0.0. - -See `BackwardCompatPrepared` for more info. -+/ -deprecated("This is provided ONLY as a temporary aid in upgrading to mysql-native v2.0.0. You should migrate from this to the Prepared-compatible exec/query overloads in 'mysql.commands'.") -BackwardCompatPrepared prepareBackwardCompat(Connection conn, const(char[]) sql) -{ - return prepareBackwardCompatImpl(conn, sql); -} - -/// Allow mysql-native tests to get around the deprecation message -package BackwardCompatPrepared prepareBackwardCompatImpl(Connection conn, const(char[]) sql) -{ - return BackwardCompatPrepared(conn, prepare(conn, sql)); -} - -/++ -Convenience function to create a prepared statement which calls a stored function. - -Be careful that your `numArgs` is correct. If it isn't, you may get a -`mysql.exceptions.MYX` with a very unclear error message. - -Throws: `mysql.exceptions.MYX` if the server has a problem. - -Params: - name = The name of the stored function. - numArgs = The number of arguments the stored procedure takes. -+/ -Prepared prepareFunction(Connection conn, string name, int numArgs) -{ - auto sql = "select " ~ name ~ preparedPlaceholderArgs(numArgs); - return prepare(conn, sql); -} - -/// -@("prepareFunction") -debug(MYSQLN_TESTS) -unittest -{ - import mysql.test.common; - mixin(scopedCn); - - exec(cn, `DROP FUNCTION IF EXISTS hello`); - exec(cn, ` - CREATE FUNCTION hello (s CHAR(20)) - RETURNS CHAR(50) DETERMINISTIC - RETURN CONCAT('Hello ',s,'!') - `); - - auto preparedHello = prepareFunction(cn, "hello", 1); - preparedHello.setArgs("World"); - auto rs = cn.query(preparedHello).array; - assert(rs.length == 1); - assert(rs[0][0] == "Hello World!"); -} - -/++ -Convenience function to create a prepared statement which calls a stored procedure. - -OUT parameters are currently not supported. It should generally be -possible with MySQL to present them as a result set. - -Be careful that your `numArgs` is correct. If it isn't, you may get a -`mysql.exceptions.MYX` with a very unclear error message. - -Throws: `mysql.exceptions.MYX` if the server has a problem. - -Params: - name = The name of the stored procedure. - numArgs = The number of arguments the stored procedure takes. - -+/ -Prepared prepareProcedure(Connection conn, string name, int numArgs) -{ - auto sql = "call " ~ name ~ preparedPlaceholderArgs(numArgs); - return prepare(conn, sql); -} - -/// -@("prepareProcedure") -debug(MYSQLN_TESTS) -unittest -{ - import mysql.test.common; - import mysql.test.integration; - mixin(scopedCn); - initBaseTestTables(cn); - - exec(cn, `DROP PROCEDURE IF EXISTS insert2`); - exec(cn, ` - CREATE PROCEDURE insert2 (IN p1 INT, IN p2 CHAR(50)) - BEGIN - INSERT INTO basetest (intcol, stringcol) VALUES(p1, p2); - END - `); - - auto preparedInsert2 = prepareProcedure(cn, "insert2", 2); - preparedInsert2.setArgs(2001, "inserted string 1"); - cn.exec(preparedInsert2); - - auto rs = query(cn, "SELECT stringcol FROM basetest WHERE intcol=2001").array; - assert(rs.length == 1); - assert(rs[0][0] == "inserted string 1"); -} - -private string preparedPlaceholderArgs(int numArgs) -{ - auto sql = "("; - bool comma = false; - foreach(i; 0..numArgs) - { - if (comma) - sql ~= ",?"; - else - { - sql ~= "?"; - comma = true; - } - } - sql ~= ")"; - - return sql; -} - -@("preparedPlaceholderArgs") -debug(MYSQLN_TESTS) -unittest -{ - assert(preparedPlaceholderArgs(3) == "(?,?,?)"); - assert(preparedPlaceholderArgs(2) == "(?,?)"); - assert(preparedPlaceholderArgs(1) == "(?)"); - assert(preparedPlaceholderArgs(0) == "()"); -} - -/// Per-connection info from the server about a registered prepared statement. -package struct PreparedServerInfo -{ - /// Server's identifier for this prepared statement. - /// Apperently, this is never 0 if it's been registered, - /// although mysql-native no longer relies on that. - uint statementId; - - ushort psWarnings; - - /// Number of parameters this statement takes. - /// - /// This will be the same on all connections, but it's returned - /// by the server upon registration, so it's stored here. - ushort numParams; - - /// Prepared statement headers - /// - /// This will be the same on all connections, but it's returned - /// by the server upon registration, so it's stored here. - PreparedStmtHeaders headers; - - /// Not actually from the server. Connection uses this to keep track - /// of statements that should be treated as having been released. - bool queuedForRelease = false; -} - -/++ -This is a wrapper over `mysql.prepared.Prepared`, provided ONLY as a -temporary aid in upgrading to mysql-native v2.0.0 and its -new connection-independent model of prepared statements. See the -$(LINK2 https://github.com/mysql-d/mysql-native/blob/master/MIGRATING_TO_V2.md, migration guide) -for more info. - -In most cases, this layer shouldn't even be needed. But if you have many -lines of code making calls to exec/query the same prepared statement, -then this may be helpful. - -To use this temporary compatability layer, change instances of: - ---- -auto stmt = conn.prepare(...); ---- - -to this: - ---- -auto stmt = conn.prepareBackwardCompat(...); ---- - -And then your prepared statement should work as before. - -BUT DO NOT LEAVE IT LIKE THIS! Ultimately, you should update -your prepared statement code to the mysql-native v2.0.0 API, by changing -instances of: - ---- -stmt.exec() -stmt.query() -stmt.queryRow() -stmt.queryRowTuple(outputArgs...) -stmt.queryValue() ---- - -to this: - ---- -conn.exec(stmt) -conn.query(stmt) -conn.queryRow(stmt) -conn.queryRowTuple(stmt, outputArgs...) -conn.queryValue(stmt) ---- - -Both of the above syntaxes can be used with a `BackwardCompatPrepared` -(the `Connection` passed directly to `mysql.commands.exec`/`mysql.commands.query` -will override the one embedded associated with your `BackwardCompatPrepared`). - -Once all of your code is updated, you can change `prepareBackwardCompat` -back to `prepare` again, and your upgrade will be complete. -+/ -struct BackwardCompatPrepared -{ - import std.variant; - - private Connection _conn; - Prepared _prepared; - - /// Access underlying `Prepared` - @property Prepared prepared() { return _prepared; } - - alias _prepared this; - - /++ - This function is provided ONLY as a temporary aid in upgrading to mysql-native v2.0.0. - - See `BackwardCompatPrepared` for more info. - +/ - deprecated("Change 'preparedStmt.exec()' to 'conn.exec(preparedStmt)'") - ulong exec() - { - return .exec(_conn, _prepared); - } - - ///ditto - deprecated("Change 'preparedStmt.query()' to 'conn.query(preparedStmt)'") - ResultRange query() - { - return .query(_conn, _prepared).unsafe; - } - - ///ditto - deprecated("Change 'preparedStmt.queryRow()' to 'conn.queryRow(preparedStmt)'") - Nullable!Row queryRow() - { - return .queryRow(_conn, _prepared).unsafe; - } - - ///ditto - deprecated("Change 'preparedStmt.queryRowTuple(outArgs...)' to 'conn.queryRowTuple(preparedStmt, outArgs...)'") - void queryRowTuple(T...)(ref T args) if(T.length == 0 || !is(T[0] : Connection)) - { - return .queryRowTuple(_conn, _prepared, args); - } - - ///ditto - deprecated("Change 'preparedStmt.queryValue()' to 'conn.queryValue(preparedStmt)'") - Nullable!Variant queryValue() @system - { - return .queryValue(_conn, _prepared).asVariant; - } -} - -/++ -A class representing a database connection. - -If you are using Vibe.d, consider using `mysql.pool.MySQLPool` instead of -creating a new Connection directly. That will provide certain benefits, -such as reusing old connections and automatic cleanup (no need to close -the connection when done). - ------------------- -// Suggested usage: - -{ - auto con = new Connection("host=localhost;port=3306;user=joe;pwd=pass123;db=myappsdb"); - scope(exit) con.close(); - - // Use the connection - ... -} ------------------- -+/ -//TODO: All low-level commms should be moved into the mysql.protocol package. -class Connection -{ - @safe: -/+ -The Connection is responsible for handshaking with the server to establish -authentication. It then passes client preferences to the server, and -subsequently is the channel for all command packets that are sent, and all -response packets received. - -Uncompressed packets consist of a 4 byte header - 3 bytes of length, and one -byte as a packet number. Connection deals with the headers and ensures that -packet numbers are sequential. - -The initial packet is sent by the server - essentially a 'hello' packet -inviting login. That packet has a sequence number of zero. That sequence -number is the incremented by client and server packets through the handshake -sequence. - -After login all further sequences are initialized by the client sending a -command packet with a zero sequence number, to which the server replies with -zero or more packets with sequential sequence numbers. -+/ -package: - enum OpenState - { - /// We have not yet connected to the server, or have sent QUIT to the - /// server and closed the connection - notConnected, - /// We have connected to the server and parsed the greeting, but not - /// yet authenticated - connected, - /// We have successfully authenticated against the server, and need to - /// send QUIT to the server when closing the connection - authenticated - } - OpenState _open; - MySQLSocket _socket; - - SvrCapFlags _sCaps, _cCaps; - uint _sThread; - ushort _serverStatus; - ubyte _sCharSet, _protocol; - string _serverVersion; - - string _host, _user, _pwd, _db; - ushort _port; - - MySQLSocketType _socketType; - - OpenSocketCallbackPhobos _openSocketPhobos; - OpenSocketCallbackVibeD _openSocketVibeD; - - ulong _insertID; - - // This gets incremented every time a command is issued or results are purged, - // so a ResultRange can tell whether it's been invalidated. - ulong _lastCommandID; - - // Whether there are rows, headers or bimary data waiting to be retreived. - // MySQL protocol doesn't permit performing any other action until all - // such data is read. - bool _rowsPending, _headersPending, _binaryPending; - - // Field count of last performed command. - //TODO: Does Connection need to store this? - ushort _fieldCount; - - // ResultSetHeaders of last performed command. - //TODO: Does Connection need to store this? Is this even used? - ResultSetHeaders _rsh; - - // This tiny thing here is pretty critical. Pay great attention to it's maintenance, otherwise - // you'll get the dreaded "packet out of order" message. It, and the socket connection are - // the reason why most other objects require a connection object for their construction. - ubyte _cpn; /// Packet Number in packet header. Serial number to ensure correct - /// ordering. First packet should have 0 - @property ubyte pktNumber() { return _cpn; } - void bumpPacket() { _cpn++; } - void resetPacket() { _cpn = 0; } - - version(Have_vibe_core) {} else - pure const nothrow invariant() - { - assert(_socketType != MySQLSocketType.vibed); - } - - static PlainPhobosSocket defaultOpenSocketPhobos(string host, ushort port) - { - auto s = new PlainPhobosSocket(); - s.connect(new InternetAddress(host, port)); - return s; - } - - static PlainVibeDSocket defaultOpenSocketVibeD(string host, ushort port) - { - version(Have_vibe_core) - return vibe.core.net.connectTCP(host, port); - else - assert(0); - } - - void initConnection() - { - kill(); // Ensure internal state gets reset - - resetPacket(); - final switch(_socketType) - { - case MySQLSocketType.phobos: - _socket = new MySQLSocketPhobos(_openSocketPhobos(_host, _port)); - break; - - case MySQLSocketType.vibed: - version(Have_vibe_core) { - _socket = new MySQLSocketVibeD(_openSocketVibeD(_host, _port)); - break; - } else assert(0, "Unsupported socket type. Need version Have_vibe_core."); - } - } - - SvrCapFlags _clientCapabilities; - - void connect(SvrCapFlags clientCapabilities) - out - { - assert(_open == OpenState.authenticated); - } - body - { - initConnection(); - auto greeting = this.parseGreeting(); - _open = OpenState.connected; - - _clientCapabilities = clientCapabilities; - _cCaps = setClientFlags(_sCaps, clientCapabilities); - this.authenticate(greeting); - } - - /++ - Forcefully close the socket without sending the quit command. - - Also resets internal state regardless of whether the connection is open or not. - - Needed in case an error leaves communatations in an undefined or non-recoverable state. - +/ - void kill() - { - if(_socket && _socket.connected) - _socket.close(); - _open = OpenState.notConnected; - // any pending data is gone. Any statements to release will be released - // on the server automatically. - _headersPending = _rowsPending = _binaryPending = false; - - preparedRegistrations.clear(); - - _lastCommandID++; // Invalidate result sets - } - - /// Called whenever mysql-native needs to send a command to the server - /// and be sure there aren't any pending results (which would prevent - /// a new command from being sent). - void autoPurge() - { - // This is called every time a command is sent, - // so detect & prevent infinite recursion. - static bool isAutoPurging = false; - - if(isAutoPurging) - return; - - isAutoPurging = true; - scope(exit) isAutoPurging = false; - - try - { - purgeResult(); - releaseQueued(); - } - catch(Exception e) - { - // Likely the connection was closed, so reset any state (and force-close if needed). - // Don't treat this as a real error, because everything will be reset when we - // reconnect. - kill(); - } - } - - /// Lookup per-connection prepared statement info by SQL - private PreparedRegistrations!PreparedServerInfo preparedRegistrations; - - /// Releases all prepared statements that are queued for release. - void releaseQueued() - { - foreach(sql, info; preparedRegistrations.directLookup) - if(info.queuedForRelease) - { - immediateReleasePrepared(this, info.statementId); - preparedRegistrations.directLookup.remove(sql); - } - } - - /// Returns null if not found - Nullable!PreparedServerInfo getPreparedServerInfo(const(char[]) sql) pure nothrow - { - return preparedRegistrations[sql]; - } - - /// If already registered, simply returns the cached `PreparedServerInfo`. - PreparedServerInfo registerIfNeeded(const(char[]) sql) - { - return preparedRegistrations.registerIfNeeded(sql, sql => performRegister(this, sql)); - } - -public: - /++ - Construct opened connection. - - Throws `mysql.exceptions.MYX` upon failure to connect. - - If you are using Vibe.d, consider using `mysql.pool.MySQLPool` instead of - creating a new Connection directly. That will provide certain benefits, - such as reusing old connections and automatic cleanup (no need to close - the connection when done). - - ------------------ - // Suggested usage: - - { - auto con = new Connection("host=localhost;port=3306;user=joe;pwd=pass123;db=myappsdb"); - scope(exit) con.close(); - - // Use the connection - ... - } - ------------------ - - Params: - cs = A connection string of the form "host=localhost;user=user;pwd=password;db=mysqld" - (TODO: The connection string needs work to allow for semicolons in its parts!) - socketType = Whether to use a Phobos or Vibe.d socket. Default is Phobos, - unless compiled with `-version=Have_vibe_core` (set automatically - if using $(LINK2 http://code.dlang.org/getting_started, DUB)). - openSocket = Optional callback which should return a newly-opened Phobos - or Vibe.d TCP socket. This allows custom sockets to be used, - subclassed from Phobos's or Vibe.d's sockets. - host = An IP address in numeric dotted form, or as a host name. - user = The user name to authenticate. - password = User's password. - db = Desired initial database. - capFlags = The set of flag bits from the server's capabilities that the client requires - +/ - //After the connection is created, and the initial invitation is received from the server - //client preferences can be set, and authentication can then be attempted. - this(string host, string user, string pwd, string db, ushort port = 3306, SvrCapFlags capFlags = defaultClientFlags) - { - version(Have_vibe_core) - enum defaultSocketType = MySQLSocketType.vibed; - else - enum defaultSocketType = MySQLSocketType.phobos; - - this(defaultSocketType, host, user, pwd, db, port, capFlags); - } - - ///ditto - this(MySQLSocketType socketType, string host, string user, string pwd, string db, ushort port = 3306, SvrCapFlags capFlags = defaultClientFlags) - { - version(Have_vibe_core) {} else - enforce!MYX(socketType != MySQLSocketType.vibed, "Cannot use Vibe.d sockets without -version=Have_vibe_core"); - - this(socketType, &defaultOpenSocketPhobos, &defaultOpenSocketVibeD, - host, user, pwd, db, port, capFlags); - } - - ///ditto - this(OpenSocketCallbackPhobos openSocket, - string host, string user, string pwd, string db, ushort port = 3306, SvrCapFlags capFlags = defaultClientFlags) - { - this(MySQLSocketType.phobos, openSocket, null, host, user, pwd, db, port, capFlags); - } - - version(Have_vibe_core) - ///ditto - this(OpenSocketCallbackVibeD openSocket, - string host, string user, string pwd, string db, ushort port = 3306, SvrCapFlags capFlags = defaultClientFlags) - { - this(MySQLSocketType.vibed, null, openSocket, host, user, pwd, db, port, capFlags); - } - - ///ditto - private this(MySQLSocketType socketType, - OpenSocketCallbackPhobos openSocketPhobos, OpenSocketCallbackVibeD openSocketVibeD, - string host, string user, string pwd, string db, ushort port = 3306, SvrCapFlags capFlags = defaultClientFlags) - in - { - final switch(socketType) - { - case MySQLSocketType.phobos: assert(openSocketPhobos !is null); break; - case MySQLSocketType.vibed: assert(openSocketVibeD !is null); break; - } - } - body - { - enforce!MYX(capFlags & SvrCapFlags.PROTOCOL41, "This client only supports protocol v4.1"); - enforce!MYX(capFlags & SvrCapFlags.SECURE_CONNECTION, "This client only supports protocol v4.1 connection"); - version(Have_vibe_core) {} else - enforce!MYX(socketType != MySQLSocketType.vibed, "Cannot use Vibe.d sockets without -version=Have_vibe_core"); - - _socketType = socketType; - _host = host; - _user = user; - _pwd = pwd; - _db = db; - _port = port; - - _openSocketPhobos = openSocketPhobos; - _openSocketVibeD = openSocketVibeD; - - connect(capFlags); - } - - ///ditto - //After the connection is created, and the initial invitation is received from the server - //client preferences can be set, and authentication can then be attempted. - this(string cs, SvrCapFlags capFlags = defaultClientFlags) - { - string[] a = parseConnectionString(cs); - this(a[0], a[1], a[2], a[3], to!ushort(a[4]), capFlags); - } - - ///ditto - this(MySQLSocketType socketType, string cs, SvrCapFlags capFlags = defaultClientFlags) - { - string[] a = parseConnectionString(cs); - this(socketType, a[0], a[1], a[2], a[3], to!ushort(a[4]), capFlags); - } - - ///ditto - this(OpenSocketCallbackPhobos openSocket, string cs, SvrCapFlags capFlags = defaultClientFlags) - { - string[] a = parseConnectionString(cs); - this(openSocket, a[0], a[1], a[2], a[3], to!ushort(a[4]), capFlags); - } - - version(Have_vibe_core) - ///ditto - this(OpenSocketCallbackVibeD openSocket, string cs, SvrCapFlags capFlags = defaultClientFlags) - { - string[] a = parseConnectionString(cs); - this(openSocket, a[0], a[1], a[2], a[3], to!ushort(a[4]), capFlags); - } - - /++ - Check whether this `Connection` is still connected to the server, or if - the connection has been closed. - +/ - @property bool closed() - { - return _open == OpenState.notConnected || !_socket.connected; - } - - /++ - Explicitly close the connection. - - Idiomatic use as follows is suggested: - ------------------ - { - auto con = new Connection("localhost:user:password:mysqld"); - scope(exit) con.close(); - // Use the connection - ... - } - ------------------ - +/ - void close() - { - // This is a two-stage process. First tell the server we are quitting this - // connection, and then close the socket. - - if (_open == OpenState.authenticated && _socket.connected) - quit(); - - if (_open == OpenState.connected) - kill(); - resetPacket(); - } - - /++ - Reconnects to the server using the same connection settings originally - used to create the `Connection`. - - Optionally takes a `mysql.protocol.constants.SvrCapFlags`, allowing you to - reconnect using a different set of server capability flags. - - Normally, if the connection is already open, this will do nothing. However, - if you request a different set of `mysql.protocol.constants.SvrCapFlags` - then was originally used to create the `Connection`, the connection will - be closed and then reconnected using the new `mysql.protocol.constants.SvrCapFlags`. - +/ - void reconnect() - { - reconnect(_clientCapabilities); - } - - ///ditto - void reconnect(SvrCapFlags clientCapabilities) - { - bool sameCaps = clientCapabilities == _clientCapabilities; - if(!closed) - { - // Same caps as before? - if(clientCapabilities == _clientCapabilities) - return; // Nothing to do, just keep current connection - - close(); - } - - connect(clientCapabilities); - } - - // This also serves as a regression test for #167: - // ResultRange doesn't get invalidated upon reconnect - @("reconnect") - debug(MYSQLN_TESTS) - unittest - { - import std.variant; - mixin(scopedCn); - cn.exec("DROP TABLE IF EXISTS `reconnect`"); - cn.exec("CREATE TABLE `reconnect` (a INTEGER) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - cn.exec("INSERT INTO `reconnect` VALUES (1),(2),(3)"); - - enum sql = "SELECT a FROM `reconnect`"; - - // Sanity check - auto rows = cn.query(sql).array; - assert(rows[0][0] == 1); - assert(rows[1][0] == 2); - assert(rows[2][0] == 3); - - // Ensure reconnect keeps the same connection when it's supposed to - auto range = cn.query(sql); - assert(range.front[0] == 1); - cn.reconnect(); - assert(!cn.closed); // Is open? - assert(range.isValid); // Still valid? - range.popFront(); - assert(range.front[0] == 2); - - // Ensure reconnect reconnects when it's supposed to - range = cn.query(sql); - assert(range.front[0] == 1); - cn._clientCapabilities = ~cn._clientCapabilities; // Pretend that we're changing the clientCapabilities - cn.reconnect(~cn._clientCapabilities); - assert(!cn.closed); // Is open? - assert(!range.isValid); // Was invalidated? - cn.query(sql).array; // Connection still works? - - // Try manually reconnecting - range = cn.query(sql); - assert(range.front[0] == 1); - cn.connect(cn._clientCapabilities); - assert(!cn.closed); // Is open? - assert(!range.isValid); // Was invalidated? - cn.query(sql).array; // Connection still works? - - // Try manually closing and connecting - range = cn.query(sql); - assert(range.front[0] == 1); - cn.close(); - assert(cn.closed); // Is closed? - assert(!range.isValid); // Was invalidated? - cn.connect(cn._clientCapabilities); - assert(!cn.closed); // Is open? - assert(!range.isValid); // Was invalidated? - cn.query(sql).array; // Connection still works? - - // Auto-reconnect upon a command - cn.close(); - assert(cn.closed); - range = cn.query(sql); - assert(!cn.closed); - assert(range.front[0] == 1); - } - - private void quit() - in - { - assert(_open == OpenState.authenticated); - } - body - { - this.sendCmd(CommandType.QUIT, []); - // No response is sent for a quit packet - _open = OpenState.connected; - } - - /++ - Parses a connection string of the form - `"host=localhost;port=3306;user=joe;pwd=pass123;db=myappsdb"` - - Port is optional and defaults to 3306. - - Whitespace surrounding any name or value is automatically stripped. - - Returns a five-element array of strings in this order: - $(UL - $(LI [0]: host) - $(LI [1]: user) - $(LI [2]: pwd) - $(LI [3]: db) - $(LI [4]: port) - ) - - (TODO: The connection string needs work to allow for semicolons in its parts!) - +/ - //TODO: Replace the return value with a proper struct. - static string[] parseConnectionString(string cs) @safe - { - string[] rv; - rv.length = 5; - rv[4] = "3306"; // Default port - string[] a = split(cs, ";"); - foreach (s; a) - { - string[] a2 = split(s, "="); - enforce!MYX(a2.length == 2, "Bad connection string: " ~ cs); - string name = strip(a2[0]); - string val = strip(a2[1]); - switch (name) - { - case "host": - rv[0] = val; - break; - case "user": - rv[1] = val; - break; - case "pwd": - rv[2] = val; - break; - case "db": - rv[3] = val; - break; - case "port": - rv[4] = val; - break; - default: - throw new MYX("Bad connection string: " ~ cs, __FILE__, __LINE__); - } - } - return rv; - } - - /++ - Select a current database. - - Throws `mysql.exceptions.MYX` upon failure. - - Params: dbName = Name of the requested database - +/ - void selectDB(string dbName) - { - this.sendCmd(CommandType.INIT_DB, dbName); - this.getCmdResponse(); - _db = dbName; - } - - /++ - Check the server status. - - Throws `mysql.exceptions.MYX` upon failure. - - Returns: An `mysql.protocol.packets.OKErrorPacket` from which server status can be determined - +/ - OKErrorPacket pingServer() - { - this.sendCmd(CommandType.PING, []); - return this.getCmdResponse(); - } - - /++ - Refresh some feature(s) of the server. - - Throws `mysql.exceptions.MYX` upon failure. - - Returns: An `mysql.protocol.packets.OKErrorPacket` from which server status can be determined - +/ - OKErrorPacket refreshServer(RefreshFlags flags) - { - this.sendCmd(CommandType.REFRESH, [flags]); - return this.getCmdResponse(); - } - - /++ - Flush any outstanding result set elements. - - When the server responds to a command that produces a result set, it - queues the whole set of corresponding packets over the current connection. - Before that `Connection` can embark on any new command, it must receive - all of those packets and junk them. - - As of v1.1.4, this is done automatically as needed. But you can still - call this manually to force a purge to occur when you want. - - See_Also: $(LINK http://www.mysqlperformanceblog.com/2007/07/08/mysql-net_write_timeout-vs-wait_timeout-and-protocol-notes/) - +/ - ulong purgeResult() - { - return mysql.protocol.comms.purgeResult(this); - } - - /++ - Get a textual report on the server status. - - (COM_STATISTICS) - +/ - string serverStats() - { - return mysql.protocol.comms.serverStats(this); - } - - /++ - Enable multiple statement commands. - - This can be used later if this feature was not requested in the client capability flags. - - Warning: This functionality is currently untested. - - Params: on = Boolean value to turn the capability on or off. - +/ - //TODO: Need to test this - void enableMultiStatements(bool on) - { - mysql.protocol.comms.enableMultiStatements(this, on); - } - - /// Return the in-force protocol number. - @property ubyte protocol() pure const nothrow { return _protocol; } - /// Server version - @property string serverVersion() pure const nothrow { return _serverVersion; } - /// Server capability flags - @property uint serverCapabilities() pure const nothrow { return _sCaps; } - /// Server status - @property ushort serverStatus() pure const nothrow { return _serverStatus; } - /// Current character set - @property ubyte charSet() pure const nothrow { return _sCharSet; } - /// Current database - @property string currentDB() pure const nothrow { return _db; } - /// Socket type being used, Phobos or Vibe.d - @property MySQLSocketType socketType() pure const nothrow { return _socketType; } - - /// After a command that inserted a row into a table with an auto-increment - /// ID column, this method allows you to retrieve the last insert ID. - @property ulong lastInsertID() pure const nothrow { return _insertID; } - - /// This gets incremented every time a command is issued or results are purged, - /// so a `mysql.result.ResultRange` can tell whether it's been invalidated. - @property ulong lastCommandID() pure const nothrow { return _lastCommandID; } - - /// Gets whether rows are pending. - /// - /// Note, you may want `hasPending` instead. - @property bool rowsPending() pure const nothrow { return _rowsPending; } - - /// Gets whether anything (rows, headers or binary) is pending. - /// New commands cannot be sent on a connection while anything is pending - /// (the pending data will automatically be purged.) - @property bool hasPending() pure const nothrow - { - return _rowsPending || _headersPending || _binaryPending; - } - - /// Gets the result header's field descriptions. - @property FieldDescription[] resultFieldDescriptions() pure { return _rsh.fieldDescriptions; } - - /++ - Manually register a prepared statement on this connection. - - Does nothing if statement is already registered on this connection. - - Calling this is not strictly necessary, as the prepared statement will - automatically be registered upon its first use on any `Connection`. - This is provided for those who prefer eager registration over lazy - for performance reasons. - +/ - void register(Prepared prepared) - { - register(prepared.sql); - } - - ///ditto - void register(const(char[]) sql) - { - registerIfNeeded(sql); - } - - /++ - Manually release a prepared statement on this connection. - - This method tells the server that it can dispose of the information it - holds about the current prepared statement. - - Calling this is not strictly necessary. The server considers prepared - statements to be per-connection, so they'll go away when the connection - closes anyway. This is provided in case direct control is actually needed. - - If you choose to use a reference counted struct to call this automatically, - be aware that embedding reference counted structs inside garbage collectible - heap objects is dangerous and should be avoided, as it can lead to various - hidden problems, from crashes to race conditions. (See the discussion at issue - $(LINK2 https://github.com/mysql-d/mysql-native/issues/159, #159) - for details.) Instead, it may be better to simply avoid trying to manage - their release at all, as it's not usually necessary. Or to periodically - release all prepared statements, and simply allow mysql-native to - automatically re-register them upon their next use. - - Notes: - - In actuality, the server might not immediately be told to release the - statement (although `isRegistered` will still report `false`). - - This is because there could be a `mysql.result.ResultRange` with results - still pending for retrieval, and the protocol doesn't allow sending commands - (such as "release a prepared statement") to the server while data is pending. - Therefore, this function may instead queue the statement to be released - when it is safe to do so: Either the next time a result set is purged or - the next time a command (such as `mysql.commands.query` or - `mysql.commands.exec`) is performed (because such commands automatically - purge any pending results). - - This function does NOT auto-purge because, if this is ever called from - automatic resource management cleanup (refcounting, RAII, etc), that - would create ugly situations where hidden, implicit behavior triggers - an unexpected auto-purge. - +/ - void release(Prepared prepared) - { - release(prepared.sql); - } - - ///ditto - void release(const(char[]) sql) - { - //TODO: Don't queue it if nothing is pending. Just do it immediately. - // But need to be certain both situations are unittested. - preparedRegistrations.queueForRelease(sql); - } - - /++ - Manually release all prepared statements on this connection. - - While minimal, every prepared statement registered on a connection does - use up a small amount of resources in both mysql-native and on the server. - Additionally, servers can be configured - $(LINK2 https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_max_prepared_stmt_count, - to limit the number of prepared statements) - allowed on a connection at one time (the default, however - is quite high). Note also, that certain overloads of `mysql.commands.exec`, - `mysql.commands.query`, etc. register prepared statements behind-the-scenes - which are cached for quick re-use later. - - Therefore, it may occasionally be useful to clear out all prepared - statements on a connection, together with all resources used by them (or - at least leave the resources ready for garbage-collection). This function - does just that. - - Note that this is ALWAYS COMPLETELY SAFE to call, even if you still have - live prepared statements you intend to use again. This is safe because - mysql-native will automatically register or re-register prepared statements - as-needed. - - Notes: - - In actuality, the prepared statements might not be immediately released - (although `isRegistered` will still report `false` for them). - - This is because there could be a `mysql.result.ResultRange` with results - still pending for retrieval, and the protocol doesn't allow sending commands - (such as "release a prepared statement") to the server while data is pending. - Therefore, this function may instead queue the statement to be released - when it is safe to do so: Either the next time a result set is purged or - the next time a command (such as `mysql.commands.query` or - `mysql.commands.exec`) is performed (because such commands automatically - purge any pending results). - - This function does NOT auto-purge because, if this is ever called from - automatic resource management cleanup (refcounting, RAII, etc), that - would create ugly situations where hidden, implicit behavior triggers - an unexpected auto-purge. - +/ - void releaseAll() - { - preparedRegistrations.queueAllForRelease(); - } - - @("releaseAll") - debug(MYSQLN_TESTS) - unittest - { - mixin(scopedCn); - - cn.exec("DROP TABLE IF EXISTS `releaseAll`"); - cn.exec("CREATE TABLE `releaseAll` (a INTEGER) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - - auto preparedSelect = cn.prepare("SELECT * FROM `releaseAll`"); - auto preparedInsert = cn.prepare("INSERT INTO `releaseAll` (a) VALUES (1)"); - assert(cn.isRegistered(preparedSelect)); - assert(cn.isRegistered(preparedInsert)); - - cn.releaseAll(); - assert(!cn.isRegistered(preparedSelect)); - assert(!cn.isRegistered(preparedInsert)); - cn.exec("INSERT INTO `releaseAll` (a) VALUES (1)"); - assert(!cn.isRegistered(preparedSelect)); - assert(!cn.isRegistered(preparedInsert)); - - cn.exec(preparedInsert); - cn.query(preparedSelect).array; - assert(cn.isRegistered(preparedSelect)); - assert(cn.isRegistered(preparedInsert)); - - } - - /// Is the given statement registered on this connection as a prepared statement? - bool isRegistered(Prepared prepared) - { - return isRegistered( prepared.sql ); - } - - ///ditto - bool isRegistered(const(char[]) sql) - { - return isRegistered( preparedRegistrations[sql] ); - } - - ///ditto - package bool isRegistered(Nullable!PreparedServerInfo info) - { - return !info.isNull && !info.get.queuedForRelease; - } -} - -// Test register, release, isRegistered, and auto-register for prepared statements -@("autoRegistration") -debug(MYSQLN_TESTS) -unittest -{ - import mysql.connection; - import mysql.test.common; - - Prepared preparedInsert; - Prepared preparedSelect; - immutable insertSQL = "INSERT INTO `autoRegistration` VALUES (1), (2)"; - immutable selectSQL = "SELECT `val` FROM `autoRegistration`"; - int queryTupleResult; - - { - mixin(scopedCn); - - // Setup - cn.exec("DROP TABLE IF EXISTS `autoRegistration`"); - cn.exec("CREATE TABLE `autoRegistration` ( - `val` INTEGER - ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - - // Initial register - preparedInsert = cn.prepare(insertSQL); - preparedSelect = cn.prepare(selectSQL); - - // Test basic register, release, isRegistered - assert(cn.isRegistered(preparedInsert)); - assert(cn.isRegistered(preparedSelect)); - cn.release(preparedInsert); - cn.release(preparedSelect); - assert(!cn.isRegistered(preparedInsert)); - assert(!cn.isRegistered(preparedSelect)); - - // Test manual re-register - cn.register(preparedInsert); - cn.register(preparedSelect); - assert(cn.isRegistered(preparedInsert)); - assert(cn.isRegistered(preparedSelect)); - - // Test double register - cn.register(preparedInsert); - cn.register(preparedSelect); - assert(cn.isRegistered(preparedInsert)); - assert(cn.isRegistered(preparedSelect)); - - // Test double release - cn.release(preparedInsert); - cn.release(preparedSelect); - assert(!cn.isRegistered(preparedInsert)); - assert(!cn.isRegistered(preparedSelect)); - cn.release(preparedInsert); - cn.release(preparedSelect); - assert(!cn.isRegistered(preparedInsert)); - assert(!cn.isRegistered(preparedSelect)); - } - - // Note that at this point, both prepared statements still exist, - // but are no longer registered on any connection. In fact, there - // are no open connections anymore. - - // Test auto-register: exec - { - mixin(scopedCn); - - assert(!cn.isRegistered(preparedInsert)); - cn.exec(preparedInsert); - assert(cn.isRegistered(preparedInsert)); - } - - // Test auto-register: query - { - mixin(scopedCn); - - assert(!cn.isRegistered(preparedSelect)); - cn.query(preparedSelect).each(); - assert(cn.isRegistered(preparedSelect)); - } - - // Test auto-register: queryRow - { - mixin(scopedCn); - - assert(!cn.isRegistered(preparedSelect)); - cn.queryRow(preparedSelect); - assert(cn.isRegistered(preparedSelect)); - } - - // Test auto-register: queryRowTuple - { - mixin(scopedCn); - - assert(!cn.isRegistered(preparedSelect)); - cn.queryRowTuple(preparedSelect, queryTupleResult); - assert(cn.isRegistered(preparedSelect)); - } - - // Test auto-register: queryValue - { - mixin(scopedCn); - - assert(!cn.isRegistered(preparedSelect)); - cn.queryValue(preparedSelect); - assert(cn.isRegistered(preparedSelect)); - } -} - -// An attempt to reproduce issue #81: Using mysql-native driver with no default database -// I'm unable to actually reproduce the error, though. -@("issue81") -debug(MYSQLN_TESTS) -unittest -{ - import mysql.escape; - mixin(scopedCn); - - cn.exec("DROP TABLE IF EXISTS `issue81`"); - cn.exec("CREATE TABLE `issue81` (a INTEGER) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - cn.exec("INSERT INTO `issue81` (a) VALUES (1)"); - - auto cn2 = new Connection(text("host=", cn._host, ";port=", cn._port, ";user=", cn._user, ";pwd=", cn._pwd)); - scope(exit) cn2.close(); - - cn2.query("SELECT * FROM `"~mysqlEscape(cn._db).text~"`.`issue81`"); -} - -// Regression test for Issue #154: -// autoPurge can throw an exception if the socket was closed without purging -// -// This simulates a disconnect by closing the socket underneath the Connection -// object itself. -@("dropConnection") -debug(MYSQLN_TESTS) -unittest -{ - mixin(scopedCn); - - cn.exec("DROP TABLE IF EXISTS `dropConnection`"); - cn.exec("CREATE TABLE `dropConnection` ( - `val` INTEGER - ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - cn.exec("INSERT INTO `dropConnection` VALUES (1), (2), (3)"); - import mysql.prepared; - { - auto prep = cn.prepare("SELECT * FROM `dropConnection`"); - cn.query(prep); - } - // close the socket forcibly - cn._socket.close(); - // this should still work (it should reconnect). - cn.exec("DROP TABLE `dropConnection`"); -} - -/+ -Test Prepared's ability to be safely refcount-released during a GC cycle -(ie, `Connection.release` must not allocate GC memory). - -Currently disabled because it's not guaranteed to always work -(and apparently, cannot be made to work?) -For relevant discussion, see issue #159: -https://github.com/mysql-d/mysql-native/issues/159 -+/ -version(none) -debug(MYSQLN_TESTS) -{ - /// Proof-of-concept ref-counted Prepared wrapper, just for testing, - /// not really intended for actual use. - private struct RCPreparedPayload - { - Prepared prepared; - Connection conn; // Connection to be released from - - alias prepared this; - - @disable this(this); // not copyable - ~this() - { - // There are a couple calls to this dtor where `conn` happens to be null. - if(conn is null) - return; - - assert(conn.isRegistered(prepared)); - conn.release(prepared); - } - } - ///ditto - alias RCPrepared = RefCounted!(RCPreparedPayload, RefCountedAutoInitialize.no); - ///ditto - private RCPrepared rcPrepare(Connection conn, const(char[]) sql) - { - import std.algorithm.mutation : move; - - auto prepared = conn.prepare(sql); - auto payload = RCPreparedPayload(prepared, conn); - return refCounted(move(payload)); - } - - @("rcPrepared") - unittest - { - import core.memory; - mixin(scopedCn); - - cn.exec("DROP TABLE IF EXISTS `rcPrepared`"); - cn.exec("CREATE TABLE `rcPrepared` ( - `val` INTEGER - ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - cn.exec("INSERT INTO `rcPrepared` VALUES (1), (2), (3)"); - - // Define this in outer scope to guarantee data is left pending when - // RCPrepared's payload is collected. This will guarantee - // that Connection will need to queue the release. - ResultRange rows; - - void bar() - { - class Foo { RCPrepared p; } - auto foo = new Foo(); - - auto rcStmt = cn.rcPrepare("SELECT * FROM `rcPrepared`"); - foo.p = rcStmt; - rows = cn.query(rcStmt); - - /+ - At this point, there are two references to the prepared statement: - One in a `Foo` object (currently bound to `foo`), and one on the stack. - - Returning from this function will destroy the one on the stack, - and deterministically reduce the refcount to 1. - - So, right here we set `foo` to null to *keep* the Foo object's - reference to the prepared statement, but set adrift the Foo object - itself, ready to be destroyed (along with the only remaining - prepared statement reference it contains) by the next GC cycle. - - Thus, `RCPreparedPayload.~this` and `Connection.release(Prepared)` - will be executed during a GC cycle...and had better not perform - any allocations, or else...boom! - +/ - foo = null; - } - - bar(); - assert(cn.hasPending); // Ensure Connection is forced to queue the release. - GC.collect(); // `Connection.release(Prepared)` better not be allocating, or boom! - } -} +public import mysql.unsafe.connection; diff --git a/source/mysql/exceptions.d b/source/mysql/exceptions.d index 560e1b69..9842569e 100644 --- a/source/mysql/exceptions.d +++ b/source/mysql/exceptions.d @@ -162,7 +162,7 @@ debug(MYSQLN_TESTS) unittest { import std.exception; - import mysql.commands; + import mysql.safe.commands; import mysql.connection; import mysql.prepared; import mysql.test.common : scopedCn, createCn; diff --git a/source/mysql/internal/connection.d b/source/mysql/internal/connection.d new file mode 100644 index 00000000..06ebdd6a --- /dev/null +++ b/source/mysql/internal/connection.d @@ -0,0 +1,1218 @@ +/// Connect to a MySQL/MariaDB server. +module mysql.internal.connection; + +import std.algorithm; +import std.conv; +import std.exception; +import std.range; +import std.socket; +import std.string; +import std.typecons; + +import mysql.exceptions; +import mysql.protocol.comms; +import mysql.protocol.constants; +import mysql.protocol.packets; +import mysql.protocol.sockets; +import mysql.internal.result; +import mysql.internal.prepared; +import mysql.types; + +@safe: + +debug(MYSQLN_TESTS) +{ + import mysql.test.common; +} + +version(Have_vibe_core) +{ + static if(__traits(compiles, (){ import vibe.core.net; } )) + import vibe.core.net; + else + static assert(false, "mysql-native can't find Vibe.d's 'vibe.core.net'."); +} + +/// The default `mysql.protocol.constants.SvrCapFlags` used when creating a connection. +immutable SvrCapFlags defaultClientFlags = + SvrCapFlags.OLD_LONG_PASSWORD | SvrCapFlags.ALL_COLUMN_FLAGS | + SvrCapFlags.WITH_DB | SvrCapFlags.PROTOCOL41 | + SvrCapFlags.SECURE_CONNECTION;// | SvrCapFlags.MULTI_STATEMENTS | + //SvrCapFlags.MULTI_RESULTS; + +package(mysql) string preparedPlaceholderArgs(int numArgs) +{ + auto sql = "("; + bool comma = false; + foreach(i; 0..numArgs) + { + if (comma) + sql ~= ",?"; + else + { + sql ~= "?"; + comma = true; + } + } + sql ~= ")"; + + return sql; +} + +@("preparedPlaceholderArgs") +debug(MYSQLN_TESTS) +unittest +{ + assert(preparedPlaceholderArgs(3) == "(?,?,?)"); + assert(preparedPlaceholderArgs(2) == "(?,?)"); + assert(preparedPlaceholderArgs(1) == "(?)"); + assert(preparedPlaceholderArgs(0) == "()"); +} + +/// Per-connection info from the server about a registered prepared statement. +package(mysql) struct PreparedServerInfo +{ + /// Server's identifier for this prepared statement. + /// Apperently, this is never 0 if it's been registered, + /// although mysql-native no longer relies on that. + uint statementId; + + ushort psWarnings; + + /// Number of parameters this statement takes. + /// + /// This will be the same on all connections, but it's returned + /// by the server upon registration, so it's stored here. + ushort numParams; + + /// Prepared statement headers + /// + /// This will be the same on all connections, but it's returned + /// by the server upon registration, so it's stored here. + PreparedStmtHeaders headers; + + /// Not actually from the server. Connection uses this to keep track + /// of statements that should be treated as having been released. + bool queuedForRelease = false; +} + +/++ +A class representing a database connection. + +If you are using Vibe.d, consider using `mysql.pool.MySQLPool` instead of +creating a new Connection directly. That will provide certain benefits, +such as reusing old connections and automatic cleanup (no need to close +the connection when done). + +------------------ +// Suggested usage: + +{ + auto con = new Connection("host=localhost;port=3306;user=joe;pwd=pass123;db=myappsdb"); + scope(exit) con.close(); + + // Use the connection + ... +} +------------------ ++/ +//TODO: All low-level commms should be moved into the mysql.protocol package. +class Connection +{ + @safe: +/+ +The Connection is responsible for handshaking with the server to establish +authentication. It then passes client preferences to the server, and +subsequently is the channel for all command packets that are sent, and all +response packets received. + +Uncompressed packets consist of a 4 byte header - 3 bytes of length, and one +byte as a packet number. Connection deals with the headers and ensures that +packet numbers are sequential. + +The initial packet is sent by the server - essentially a 'hello' packet +inviting login. That packet has a sequence number of zero. That sequence +number is the incremented by client and server packets through the handshake +sequence. + +After login all further sequences are initialized by the client sending a +command packet with a zero sequence number, to which the server replies with +zero or more packets with sequential sequence numbers. ++/ +package(mysql): + enum OpenState + { + /// We have not yet connected to the server, or have sent QUIT to the + /// server and closed the connection + notConnected, + /// We have connected to the server and parsed the greeting, but not + /// yet authenticated + connected, + /// We have successfully authenticated against the server, and need to + /// send QUIT to the server when closing the connection + authenticated + } + OpenState _open; + MySQLSocket _socket; + + SvrCapFlags _sCaps, _cCaps; + uint _sThread; + ushort _serverStatus; + ubyte _sCharSet, _protocol; + string _serverVersion; + + string _host, _user, _pwd, _db; + ushort _port; + + MySQLSocketType _socketType; + + OpenSocketCallbackPhobos _openSocketPhobos; + OpenSocketCallbackVibeD _openSocketVibeD; + + ulong _insertID; + + // This gets incremented every time a command is issued or results are purged, + // so a ResultRange can tell whether it's been invalidated. + ulong _lastCommandID; + + // Whether there are rows, headers or bimary data waiting to be retreived. + // MySQL protocol doesn't permit performing any other action until all + // such data is read. + bool _rowsPending, _headersPending, _binaryPending; + + // Field count of last performed command. + //TODO: Does Connection need to store this? + ushort _fieldCount; + + // ResultSetHeaders of last performed command. + //TODO: Does Connection need to store this? Is this even used? + ResultSetHeaders _rsh; + + // This tiny thing here is pretty critical. Pay great attention to it's maintenance, otherwise + // you'll get the dreaded "packet out of order" message. It, and the socket connection are + // the reason why most other objects require a connection object for their construction. + ubyte _cpn; /// Packet Number in packet header. Serial number to ensure correct + /// ordering. First packet should have 0 + @property ubyte pktNumber() { return _cpn; } + void bumpPacket() { _cpn++; } + void resetPacket() { _cpn = 0; } + + version(Have_vibe_core) {} else + pure const nothrow invariant() + { + assert(_socketType != MySQLSocketType.vibed); + } + + static PlainPhobosSocket defaultOpenSocketPhobos(string host, ushort port) + { + auto s = new PlainPhobosSocket(); + s.connect(new InternetAddress(host, port)); + return s; + } + + static PlainVibeDSocket defaultOpenSocketVibeD(string host, ushort port) + { + version(Have_vibe_core) + return vibe.core.net.connectTCP(host, port); + else + assert(0); + } + + void initConnection() + { + kill(); // Ensure internal state gets reset + + resetPacket(); + final switch(_socketType) + { + case MySQLSocketType.phobos: + _socket = new MySQLSocketPhobos(_openSocketPhobos(_host, _port)); + break; + + case MySQLSocketType.vibed: + version(Have_vibe_core) { + _socket = new MySQLSocketVibeD(_openSocketVibeD(_host, _port)); + break; + } else assert(0, "Unsupported socket type. Need version Have_vibe_core."); + } + } + + SvrCapFlags _clientCapabilities; + + void connect(SvrCapFlags clientCapabilities) + out + { + assert(_open == OpenState.authenticated); + } + body + { + initConnection(); + auto greeting = this.parseGreeting(); + _open = OpenState.connected; + + _clientCapabilities = clientCapabilities; + _cCaps = setClientFlags(_sCaps, clientCapabilities); + this.authenticate(greeting); + } + + /++ + Forcefully close the socket without sending the quit command. + + Also resets internal state regardless of whether the connection is open or not. + + Needed in case an error leaves communatations in an undefined or non-recoverable state. + +/ + void kill() + { + if(_socket && _socket.connected) + _socket.close(); + _open = OpenState.notConnected; + // any pending data is gone. Any statements to release will be released + // on the server automatically. + _headersPending = _rowsPending = _binaryPending = false; + + preparedRegistrations.clear(); + + _lastCommandID++; // Invalidate result sets + } + + /// Called whenever mysql-native needs to send a command to the server + /// and be sure there aren't any pending results (which would prevent + /// a new command from being sent). + void autoPurge() + { + // This is called every time a command is sent, + // so detect & prevent infinite recursion. + static bool isAutoPurging = false; + + if(isAutoPurging) + return; + + isAutoPurging = true; + scope(exit) isAutoPurging = false; + + try + { + purgeResult(); + releaseQueued(); + } + catch(Exception e) + { + // Likely the connection was closed, so reset any state (and force-close if needed). + // Don't treat this as a real error, because everything will be reset when we + // reconnect. + kill(); + } + } + + /// Lookup per-connection prepared statement info by SQL + private PreparedRegistrations!PreparedServerInfo preparedRegistrations; + + /// Releases all prepared statements that are queued for release. + void releaseQueued() + { + foreach(sql, info; preparedRegistrations.directLookup) + if(info.queuedForRelease) + { + immediateReleasePrepared(this, info.statementId); + preparedRegistrations.directLookup.remove(sql); + } + } + + /// Returns null if not found + Nullable!PreparedServerInfo getPreparedServerInfo(const(char[]) sql) pure nothrow + { + return preparedRegistrations[sql]; + } + + /// If already registered, simply returns the cached `PreparedServerInfo`. + PreparedServerInfo registerIfNeeded(const(char[]) sql) + { + return preparedRegistrations.registerIfNeeded(sql, sql => performRegister(this, sql)); + } + +public: + + /++ + Construct opened connection. + + Throws `mysql.exceptions.MYX` upon failure to connect. + + If you are using Vibe.d, consider using `mysql.pool.MySQLPool` instead of + creating a new Connection directly. That will provide certain benefits, + such as reusing old connections and automatic cleanup (no need to close + the connection when done). + + ------------------ + // Suggested usage: + + { + auto con = new Connection("host=localhost;port=3306;user=joe;pwd=pass123;db=myappsdb"); + scope(exit) con.close(); + + // Use the connection + ... + } + ------------------ + + Params: + cs = A connection string of the form "host=localhost;user=user;pwd=password;db=mysqld" + (TODO: The connection string needs work to allow for semicolons in its parts!) + socketType = Whether to use a Phobos or Vibe.d socket. Default is Phobos, + unless compiled with `-version=Have_vibe_core` (set automatically + if using $(LINK2 http://code.dlang.org/getting_started, DUB)). + openSocket = Optional callback which should return a newly-opened Phobos + or Vibe.d TCP socket. This allows custom sockets to be used, + subclassed from Phobos's or Vibe.d's sockets. + host = An IP address in numeric dotted form, or as a host name. + user = The user name to authenticate. + password = User's password. + db = Desired initial database. + capFlags = The set of flag bits from the server's capabilities that the client requires + +/ + //After the connection is created, and the initial invitation is received from the server + //client preferences can be set, and authentication can then be attempted. + this(string host, string user, string pwd, string db, ushort port = 3306, SvrCapFlags capFlags = defaultClientFlags) + { + version(Have_vibe_core) + enum defaultSocketType = MySQLSocketType.vibed; + else + enum defaultSocketType = MySQLSocketType.phobos; + + this(defaultSocketType, host, user, pwd, db, port, capFlags); + } + + ///ditto + this(MySQLSocketType socketType, string host, string user, string pwd, string db, ushort port = 3306, SvrCapFlags capFlags = defaultClientFlags) + { + version(Have_vibe_core) {} else + enforce!MYX(socketType != MySQLSocketType.vibed, "Cannot use Vibe.d sockets without -version=Have_vibe_core"); + + this(socketType, &defaultOpenSocketPhobos, &defaultOpenSocketVibeD, + host, user, pwd, db, port, capFlags); + } + + ///ditto + this(OpenSocketCallbackPhobos openSocket, + string host, string user, string pwd, string db, ushort port = 3306, SvrCapFlags capFlags = defaultClientFlags) + { + this(MySQLSocketType.phobos, openSocket, null, host, user, pwd, db, port, capFlags); + } + + version(Have_vibe_core) + ///ditto + this(OpenSocketCallbackVibeD openSocket, + string host, string user, string pwd, string db, ushort port = 3306, SvrCapFlags capFlags = defaultClientFlags) + { + this(MySQLSocketType.vibed, null, openSocket, host, user, pwd, db, port, capFlags); + } + + ///ditto + private this(MySQLSocketType socketType, + OpenSocketCallbackPhobos openSocketPhobos, OpenSocketCallbackVibeD openSocketVibeD, + string host, string user, string pwd, string db, ushort port = 3306, SvrCapFlags capFlags = defaultClientFlags) + in + { + final switch(socketType) + { + case MySQLSocketType.phobos: assert(openSocketPhobos !is null); break; + case MySQLSocketType.vibed: assert(openSocketVibeD !is null); break; + } + } + body + { + enforce!MYX(capFlags & SvrCapFlags.PROTOCOL41, "This client only supports protocol v4.1"); + enforce!MYX(capFlags & SvrCapFlags.SECURE_CONNECTION, "This client only supports protocol v4.1 connection"); + version(Have_vibe_core) {} else + enforce!MYX(socketType != MySQLSocketType.vibed, "Cannot use Vibe.d sockets without -version=Have_vibe_core"); + + _socketType = socketType; + _host = host; + _user = user; + _pwd = pwd; + _db = db; + _port = port; + + _openSocketPhobos = openSocketPhobos; + _openSocketVibeD = openSocketVibeD; + + connect(capFlags); + } + + ///ditto + //After the connection is created, and the initial invitation is received from the server + //client preferences can be set, and authentication can then be attempted. + this(string cs, SvrCapFlags capFlags = defaultClientFlags) + { + string[] a = parseConnectionString(cs); + this(a[0], a[1], a[2], a[3], to!ushort(a[4]), capFlags); + } + + ///ditto + this(MySQLSocketType socketType, string cs, SvrCapFlags capFlags = defaultClientFlags) + { + string[] a = parseConnectionString(cs); + this(socketType, a[0], a[1], a[2], a[3], to!ushort(a[4]), capFlags); + } + + ///ditto + this(OpenSocketCallbackPhobos openSocket, string cs, SvrCapFlags capFlags = defaultClientFlags) + { + string[] a = parseConnectionString(cs); + this(openSocket, a[0], a[1], a[2], a[3], to!ushort(a[4]), capFlags); + } + + version(Have_vibe_core) + ///ditto + this(OpenSocketCallbackVibeD openSocket, string cs, SvrCapFlags capFlags = defaultClientFlags) + { + string[] a = parseConnectionString(cs); + this(openSocket, a[0], a[1], a[2], a[3], to!ushort(a[4]), capFlags); + } + + /++ + Check whether this `Connection` is still connected to the server, or if + the connection has been closed. + +/ + @property bool closed() + { + return _open == OpenState.notConnected || !_socket.connected; + } + + /++ + Explicitly close the connection. + + Idiomatic use as follows is suggested: + ------------------ + { + auto con = new Connection("localhost:user:password:mysqld"); + scope(exit) con.close(); + // Use the connection + ... + } + ------------------ + +/ + void close() + { + // This is a two-stage process. First tell the server we are quitting this + // connection, and then close the socket. + + if (_open == OpenState.authenticated && _socket.connected) + quit(); + + if (_open == OpenState.connected) + kill(); + resetPacket(); + } + + /++ + Reconnects to the server using the same connection settings originally + used to create the `Connection`. + + Optionally takes a `mysql.protocol.constants.SvrCapFlags`, allowing you to + reconnect using a different set of server capability flags. + + Normally, if the connection is already open, this will do nothing. However, + if you request a different set of `mysql.protocol.constants.SvrCapFlags` + then was originally used to create the `Connection`, the connection will + be closed and then reconnected using the new `mysql.protocol.constants.SvrCapFlags`. + +/ + void reconnect() + { + reconnect(_clientCapabilities); + } + + ///ditto + void reconnect(SvrCapFlags clientCapabilities) + { + bool sameCaps = clientCapabilities == _clientCapabilities; + if(!closed) + { + // Same caps as before? + if(clientCapabilities == _clientCapabilities) + return; // Nothing to do, just keep current connection + + close(); + } + + connect(clientCapabilities); + } + + // This also serves as a regression test for #167: + // ResultRange doesn't get invalidated upon reconnect + @("reconnect") + debug(MYSQLN_TESTS) + unittest + { + import std.variant; + import mysql.safe.commands; + mixin(scopedCn); + cn.exec("DROP TABLE IF EXISTS `reconnect`"); + cn.exec("CREATE TABLE `reconnect` (a INTEGER) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + cn.exec("INSERT INTO `reconnect` VALUES (1),(2),(3)"); + + enum sql = "SELECT a FROM `reconnect`"; + + // Sanity check + auto rows = cn.query(sql).array; + assert(rows[0][0] == 1); + assert(rows[1][0] == 2); + assert(rows[2][0] == 3); + + // Ensure reconnect keeps the same connection when it's supposed to + auto range = cn.query(sql); + assert(range.front[0] == 1); + cn.reconnect(); + assert(!cn.closed); // Is open? + assert(range.isValid); // Still valid? + range.popFront(); + assert(range.front[0] == 2); + + // Ensure reconnect reconnects when it's supposed to + range = cn.query(sql); + assert(range.front[0] == 1); + cn._clientCapabilities = ~cn._clientCapabilities; // Pretend that we're changing the clientCapabilities + cn.reconnect(~cn._clientCapabilities); + assert(!cn.closed); // Is open? + assert(!range.isValid); // Was invalidated? + cn.query(sql).array; // Connection still works? + + // Try manually reconnecting + range = cn.query(sql); + assert(range.front[0] == 1); + cn.connect(cn._clientCapabilities); + assert(!cn.closed); // Is open? + assert(!range.isValid); // Was invalidated? + cn.query(sql).array; // Connection still works? + + // Try manually closing and connecting + range = cn.query(sql); + assert(range.front[0] == 1); + cn.close(); + assert(cn.closed); // Is closed? + assert(!range.isValid); // Was invalidated? + cn.connect(cn._clientCapabilities); + assert(!cn.closed); // Is open? + assert(!range.isValid); // Was invalidated? + cn.query(sql).array; // Connection still works? + + // Auto-reconnect upon a command + cn.close(); + assert(cn.closed); + range = cn.query(sql); + assert(!cn.closed); + assert(range.front[0] == 1); + } + + private void quit() + in + { + assert(_open == OpenState.authenticated); + } + body + { + this.sendCmd(CommandType.QUIT, []); + // No response is sent for a quit packet + _open = OpenState.connected; + } + + /++ + Parses a connection string of the form + `"host=localhost;port=3306;user=joe;pwd=pass123;db=myappsdb"` + + Port is optional and defaults to 3306. + + Whitespace surrounding any name or value is automatically stripped. + + Returns a five-element array of strings in this order: + $(UL + $(LI [0]: host) + $(LI [1]: user) + $(LI [2]: pwd) + $(LI [3]: db) + $(LI [4]: port) + ) + + (TODO: The connection string needs work to allow for semicolons in its parts!) + +/ + //TODO: Replace the return value with a proper struct. + static string[] parseConnectionString(string cs) @safe + { + string[] rv; + rv.length = 5; + rv[4] = "3306"; // Default port + string[] a = split(cs, ";"); + foreach (s; a) + { + string[] a2 = split(s, "="); + enforce!MYX(a2.length == 2, "Bad connection string: " ~ cs); + string name = strip(a2[0]); + string val = strip(a2[1]); + switch (name) + { + case "host": + rv[0] = val; + break; + case "user": + rv[1] = val; + break; + case "pwd": + rv[2] = val; + break; + case "db": + rv[3] = val; + break; + case "port": + rv[4] = val; + break; + default: + throw new MYX("Bad connection string: " ~ cs, __FILE__, __LINE__); + } + } + return rv; + } + + /++ + Select a current database. + + Throws `mysql.exceptions.MYX` upon failure. + + Params: dbName = Name of the requested database + +/ + void selectDB(string dbName) + { + this.sendCmd(CommandType.INIT_DB, dbName); + this.getCmdResponse(); + _db = dbName; + } + + /++ + Check the server status. + + Throws `mysql.exceptions.MYX` upon failure. + + Returns: An `mysql.protocol.packets.OKErrorPacket` from which server status can be determined + +/ + OKErrorPacket pingServer() + { + this.sendCmd(CommandType.PING, []); + return this.getCmdResponse(); + } + + /++ + Refresh some feature(s) of the server. + + Throws `mysql.exceptions.MYX` upon failure. + + Returns: An `mysql.protocol.packets.OKErrorPacket` from which server status can be determined + +/ + OKErrorPacket refreshServer(RefreshFlags flags) + { + this.sendCmd(CommandType.REFRESH, [flags]); + return this.getCmdResponse(); + } + + /++ + Flush any outstanding result set elements. + + When the server responds to a command that produces a result set, it + queues the whole set of corresponding packets over the current connection. + Before that `Connection` can embark on any new command, it must receive + all of those packets and junk them. + + As of v1.1.4, this is done automatically as needed. But you can still + call this manually to force a purge to occur when you want. + + See_Also: $(LINK http://www.mysqlperformanceblog.com/2007/07/08/mysql-net_write_timeout-vs-wait_timeout-and-protocol-notes/) + +/ + ulong purgeResult() + { + return mysql.protocol.comms.purgeResult(this); + } + + /++ + Get a textual report on the server status. + + (COM_STATISTICS) + +/ + string serverStats() + { + return mysql.protocol.comms.serverStats(this); + } + + /++ + Enable multiple statement commands. + + This can be used later if this feature was not requested in the client capability flags. + + Warning: This functionality is currently untested. + + Params: on = Boolean value to turn the capability on or off. + +/ + //TODO: Need to test this + void enableMultiStatements(bool on) + { + mysql.protocol.comms.enableMultiStatements(this, on); + } + + /// Return the in-force protocol number. + @property ubyte protocol() pure const nothrow { return _protocol; } + /// Server version + @property string serverVersion() pure const nothrow { return _serverVersion; } + /// Server capability flags + @property uint serverCapabilities() pure const nothrow { return _sCaps; } + /// Server status + @property ushort serverStatus() pure const nothrow { return _serverStatus; } + /// Current character set + @property ubyte charSet() pure const nothrow { return _sCharSet; } + /// Current database + @property string currentDB() pure const nothrow { return _db; } + /// Socket type being used, Phobos or Vibe.d + @property MySQLSocketType socketType() pure const nothrow { return _socketType; } + + /// After a command that inserted a row into a table with an auto-increment + /// ID column, this method allows you to retrieve the last insert ID. + @property ulong lastInsertID() pure const nothrow { return _insertID; } + + /// This gets incremented every time a command is issued or results are purged, + /// so a `mysql.result.ResultRange` can tell whether it's been invalidated. + @property ulong lastCommandID() pure const nothrow { return _lastCommandID; } + + /// Gets whether rows are pending. + /// + /// Note, you may want `hasPending` instead. + @property bool rowsPending() pure const nothrow { return _rowsPending; } + + /// Gets whether anything (rows, headers or binary) is pending. + /// New commands cannot be sent on a connection while anything is pending + /// (the pending data will automatically be purged.) + @property bool hasPending() pure const nothrow + { + return _rowsPending || _headersPending || _binaryPending; + } + + /// Gets the result header's field descriptions. + @property FieldDescription[] resultFieldDescriptions() pure { return _rsh.fieldDescriptions; } + + /++ + Manually register a prepared statement on this connection. + + Does nothing if statement is already registered on this connection. + + Calling this is not strictly necessary, as the prepared statement will + automatically be registered upon its first use on any `Connection`. + This is provided for those who prefer eager registration over lazy + for performance reasons. + +/ + void register(SafePrepared prepared) + { + register(prepared.sql); + } + + ///ditto + void register(const(char[]) sql) + { + registerIfNeeded(sql); + } + + /++ + Manually release a prepared statement on this connection. + + This method tells the server that it can dispose of the information it + holds about the current prepared statement. + + Calling this is not strictly necessary. The server considers prepared + statements to be per-connection, so they'll go away when the connection + closes anyway. This is provided in case direct control is actually needed. + + If you choose to use a reference counted struct to call this automatically, + be aware that embedding reference counted structs inside garbage collectible + heap objects is dangerous and should be avoided, as it can lead to various + hidden problems, from crashes to race conditions. (See the discussion at issue + $(LINK2 https://github.com/mysql-d/mysql-native/issues/159, #159) + for details.) Instead, it may be better to simply avoid trying to manage + their release at all, as it's not usually necessary. Or to periodically + release all prepared statements, and simply allow mysql-native to + automatically re-register them upon their next use. + + Notes: + + In actuality, the server might not immediately be told to release the + statement (although `isRegistered` will still report `false`). + + This is because there could be a `mysql.result.ResultRange` with results + still pending for retrieval, and the protocol doesn't allow sending commands + (such as "release a prepared statement") to the server while data is pending. + Therefore, this function may instead queue the statement to be released + when it is safe to do so: Either the next time a result set is purged or + the next time a command (such as `mysql.commands.query` or + `mysql.commands.exec`) is performed (because such commands automatically + purge any pending results). + + This function does NOT auto-purge because, if this is ever called from + automatic resource management cleanup (refcounting, RAII, etc), that + would create ugly situations where hidden, implicit behavior triggers + an unexpected auto-purge. + +/ + void release(SafePrepared prepared) + { + release(prepared.sql); + } + + ///ditto + void release(const(char[]) sql) + { + //TODO: Don't queue it if nothing is pending. Just do it immediately. + // But need to be certain both situations are unittested. + preparedRegistrations.queueForRelease(sql); + } + + /++ + Manually release all prepared statements on this connection. + + While minimal, every prepared statement registered on a connection does + use up a small amount of resources in both mysql-native and on the server. + Additionally, servers can be configured + $(LINK2 https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_max_prepared_stmt_count, + to limit the number of prepared statements) + allowed on a connection at one time (the default, however + is quite high). Note also, that certain overloads of `mysql.commands.exec`, + `mysql.commands.query`, etc. register prepared statements behind-the-scenes + which are cached for quick re-use later. + + Therefore, it may occasionally be useful to clear out all prepared + statements on a connection, together with all resources used by them (or + at least leave the resources ready for garbage-collection). This function + does just that. + + Note that this is ALWAYS COMPLETELY SAFE to call, even if you still have + live prepared statements you intend to use again. This is safe because + mysql-native will automatically register or re-register prepared statements + as-needed. + + Notes: + + In actuality, the prepared statements might not be immediately released + (although `isRegistered` will still report `false` for them). + + This is because there could be a `mysql.result.ResultRange` with results + still pending for retrieval, and the protocol doesn't allow sending commands + (such as "release a prepared statement") to the server while data is pending. + Therefore, this function may instead queue the statement to be released + when it is safe to do so: Either the next time a result set is purged or + the next time a command (such as `mysql.commands.query` or + `mysql.commands.exec`) is performed (because such commands automatically + purge any pending results). + + This function does NOT auto-purge because, if this is ever called from + automatic resource management cleanup (refcounting, RAII, etc), that + would create ugly situations where hidden, implicit behavior triggers + an unexpected auto-purge. + +/ + void releaseAll() + { + preparedRegistrations.queueAllForRelease(); + } + + @("releaseAll") + debug(MYSQLN_TESTS) + unittest + { + import mysql.safe.commands; + import mysql.safe.connection; + mixin(scopedCn); + + cn.exec("DROP TABLE IF EXISTS `releaseAll`"); + cn.exec("CREATE TABLE `releaseAll` (a INTEGER) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + + auto preparedSelect = cn.prepare("SELECT * FROM `releaseAll`"); + auto preparedInsert = cn.prepare("INSERT INTO `releaseAll` (a) VALUES (1)"); + assert(cn.isRegistered(preparedSelect)); + assert(cn.isRegistered(preparedInsert)); + + cn.releaseAll(); + assert(!cn.isRegistered(preparedSelect)); + assert(!cn.isRegistered(preparedInsert)); + cn.exec("INSERT INTO `releaseAll` (a) VALUES (1)"); + assert(!cn.isRegistered(preparedSelect)); + assert(!cn.isRegistered(preparedInsert)); + + cn.exec(preparedInsert); + cn.query(preparedSelect).array; + assert(cn.isRegistered(preparedSelect)); + assert(cn.isRegistered(preparedInsert)); + + } + + /// Is the given statement registered on this connection as a prepared statement? + bool isRegistered(SafePrepared prepared) + { + return isRegistered( prepared.sql ); + } + + ///ditto + bool isRegistered(const(char[]) sql) + { + return isRegistered( preparedRegistrations[sql] ); + } + + ///ditto + package bool isRegistered(Nullable!PreparedServerInfo info) + { + return !info.isNull && !info.get.queuedForRelease; + } +} + +// Test register, release, isRegistered, and auto-register for prepared statements +@("autoRegistration") +debug(MYSQLN_TESTS) +unittest +{ + import mysql.connection; + import mysql.test.common; + import mysql.safe.prepared; + import mysql.safe.commands; + + Prepared preparedInsert; + Prepared preparedSelect; + immutable insertSQL = "INSERT INTO `autoRegistration` VALUES (1), (2)"; + immutable selectSQL = "SELECT `val` FROM `autoRegistration`"; + int queryTupleResult; + + { + mixin(scopedCn); + + // Setup + cn.exec("DROP TABLE IF EXISTS `autoRegistration`"); + cn.exec("CREATE TABLE `autoRegistration` ( + `val` INTEGER + ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + + // Initial register + preparedInsert = cn.prepare(insertSQL); + preparedSelect = cn.prepare(selectSQL); + + // Test basic register, release, isRegistered + assert(cn.isRegistered(preparedInsert)); + assert(cn.isRegistered(preparedSelect)); + cn.release(preparedInsert); + cn.release(preparedSelect); + assert(!cn.isRegistered(preparedInsert)); + assert(!cn.isRegistered(preparedSelect)); + + // Test manual re-register + cn.register(preparedInsert); + cn.register(preparedSelect); + assert(cn.isRegistered(preparedInsert)); + assert(cn.isRegistered(preparedSelect)); + + // Test double register + cn.register(preparedInsert); + cn.register(preparedSelect); + assert(cn.isRegistered(preparedInsert)); + assert(cn.isRegistered(preparedSelect)); + + // Test double release + cn.release(preparedInsert); + cn.release(preparedSelect); + assert(!cn.isRegistered(preparedInsert)); + assert(!cn.isRegistered(preparedSelect)); + cn.release(preparedInsert); + cn.release(preparedSelect); + assert(!cn.isRegistered(preparedInsert)); + assert(!cn.isRegistered(preparedSelect)); + } + + // Note that at this point, both prepared statements still exist, + // but are no longer registered on any connection. In fact, there + // are no open connections anymore. + + // Test auto-register: exec + { + mixin(scopedCn); + + assert(!cn.isRegistered(preparedInsert)); + cn.exec(preparedInsert); + assert(cn.isRegistered(preparedInsert)); + } + + // Test auto-register: query + { + mixin(scopedCn); + + assert(!cn.isRegistered(preparedSelect)); + cn.query(preparedSelect).each(); + assert(cn.isRegistered(preparedSelect)); + } + + // Test auto-register: queryRow + { + mixin(scopedCn); + + assert(!cn.isRegistered(preparedSelect)); + cn.queryRow(preparedSelect); + assert(cn.isRegistered(preparedSelect)); + } + + // Test auto-register: queryRowTuple + { + mixin(scopedCn); + + assert(!cn.isRegistered(preparedSelect)); + cn.queryRowTuple(preparedSelect, queryTupleResult); + assert(cn.isRegistered(preparedSelect)); + } + + // Test auto-register: queryValue + { + mixin(scopedCn); + + assert(!cn.isRegistered(preparedSelect)); + cn.queryValue(preparedSelect); + assert(cn.isRegistered(preparedSelect)); + } +} + +// An attempt to reproduce issue #81: Using mysql-native driver with no default database +// I'm unable to actually reproduce the error, though. +@("issue81") +debug(MYSQLN_TESTS) +unittest +{ + import mysql.escape; + import mysql.safe.commands; + mixin(scopedCn); + + cn.exec("DROP TABLE IF EXISTS `issue81`"); + cn.exec("CREATE TABLE `issue81` (a INTEGER) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + cn.exec("INSERT INTO `issue81` (a) VALUES (1)"); + + auto cn2 = new Connection(text("host=", cn._host, ";port=", cn._port, ";user=", cn._user, ";pwd=", cn._pwd)); + scope(exit) cn2.close(); + + cn2.query("SELECT * FROM `"~mysqlEscape(cn._db).text~"`.`issue81`"); +} + +// Regression test for Issue #154: +// autoPurge can throw an exception if the socket was closed without purging +// +// This simulates a disconnect by closing the socket underneath the Connection +// object itself. +@("dropConnection") +debug(MYSQLN_TESTS) +unittest +{ + import mysql.safe.commands; + import mysql.safe.connection; + mixin(scopedCn); + + cn.exec("DROP TABLE IF EXISTS `dropConnection`"); + cn.exec("CREATE TABLE `dropConnection` ( + `val` INTEGER + ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + cn.exec("INSERT INTO `dropConnection` VALUES (1), (2), (3)"); + import mysql.prepared; + { + auto prep = cn.prepare("SELECT * FROM `dropConnection`"); + cn.query(prep); + } + // close the socket forcibly + cn._socket.close(); + // this should still work (it should reconnect). + cn.exec("DROP TABLE `dropConnection`"); +} + +/+ +Test Prepared's ability to be safely refcount-released during a GC cycle +(ie, `Connection.release` must not allocate GC memory). + +Currently disabled because it's not guaranteed to always work +(and apparently, cannot be made to work?) +For relevant discussion, see issue #159: +https://github.com/mysql-d/mysql-native/issues/159 ++/ +version(none) +debug(MYSQLN_TESTS) +{ + /// Proof-of-concept ref-counted Prepared wrapper, just for testing, + /// not really intended for actual use. + private struct RCPreparedPayload + { + Prepared prepared; + Connection conn; // Connection to be released from + + alias prepared this; + + @disable this(this); // not copyable + ~this() + { + // There are a couple calls to this dtor where `conn` happens to be null. + if(conn is null) + return; + + assert(conn.isRegistered(prepared)); + conn.release(prepared); + } + } + ///ditto + alias RCPrepared = RefCounted!(RCPreparedPayload, RefCountedAutoInitialize.no); + ///ditto + private RCPrepared rcPrepare(Connection conn, const(char[]) sql) + { + import std.algorithm.mutation : move; + + auto prepared = conn.prepare(sql); + auto payload = RCPreparedPayload(prepared, conn); + return refCounted(move(payload)); + } + + @("rcPrepared") + unittest + { + import core.memory; + mixin(scopedCn); + + cn.exec("DROP TABLE IF EXISTS `rcPrepared`"); + cn.exec("CREATE TABLE `rcPrepared` ( + `val` INTEGER + ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + cn.exec("INSERT INTO `rcPrepared` VALUES (1), (2), (3)"); + + // Define this in outer scope to guarantee data is left pending when + // RCPrepared's payload is collected. This will guarantee + // that Connection will need to queue the release. + ResultRange rows; + + void bar() + { + class Foo { RCPrepared p; } + auto foo = new Foo(); + + auto rcStmt = cn.rcPrepare("SELECT * FROM `rcPrepared`"); + foo.p = rcStmt; + rows = cn.query(rcStmt); + + /+ + At this point, there are two references to the prepared statement: + One in a `Foo` object (currently bound to `foo`), and one on the stack. + + Returning from this function will destroy the one on the stack, + and deterministically reduce the refcount to 1. + + So, right here we set `foo` to null to *keep* the Foo object's + reference to the prepared statement, but set adrift the Foo object + itself, ready to be destroyed (along with the only remaining + prepared statement reference it contains) by the next GC cycle. + + Thus, `RCPreparedPayload.~this` and `Connection.release(Prepared)` + will be executed during a GC cycle...and had better not perform + any allocations, or else...boom! + +/ + foo = null; + } + + bar(); + assert(cn.hasPending); // Ensure Connection is forced to queue the release. + GC.collect(); // `Connection.release(Prepared)` better not be allocating, or boom! + } +} diff --git a/source/mysql/internal/pool.d b/source/mysql/internal/pool.d new file mode 100644 index 00000000..8171f40e --- /dev/null +++ b/source/mysql/internal/pool.d @@ -0,0 +1,629 @@ +/++ +Connect to a MySQL/MariaDB database using vibe.d's +$(LINK2 http://vibed.org/api/vibe.core.connectionpool/ConnectionPool, ConnectionPool). + +You have to include vibe.d in your project to be able to use this class. +If you don't want to, refer to `mysql.connection.Connection`. + +This provides various benefits over creating a new connection manually, +such as automatically reusing old connections, and automatic cleanup (no need to close +the connection when done). ++/ +module mysql.internal.pool; + +import std.conv; +import std.typecons; +import mysql.connection; +import mysql.prepared; +import mysql.protocol.constants; +debug(MYSQLN_TESTS) +{ + import mysql.test.common; +} + +version(Have_vibe_core) +{ + version = IncludeMySQLPool; + static if(is(typeof(ConnectionPool!Connection.init.removeUnused((c){})))) + version = HaveCleanupFunction; +} +version(MySQLDocs) +{ + version = IncludeMySQLPool; + version = HaveCleanupFunction; +} + +version(IncludeMySQLPool) +{ + version(Have_vibe_core) + import vibe.core.connectionpool; + else version(MySQLDocs) + { + /++ + Vibe.d's + $(LINK2 http://vibed.org/api/vibe.core.connectionpool/ConnectionPool, ConnectionPool) + class. + + Not actually included in module `mysql.pool`. Only listed here for + documentation purposes. For ConnectionPool and it's documentation, see: + $(LINK http://vibed.org/api/vibe.core.connectionpool/ConnectionPool) + +/ + class ConnectionPool(T) + { + /// See: $(LINK http://vibed.org/api/vibe.core.connectionpool/ConnectionPool.this) + this(Connection delegate() connection_factory, uint max_concurrent = (uint).max) + {} + + /// See: $(LINK http://vibed.org/api/vibe.core.connectionpool/ConnectionPool.lockConnection) + LockedConnection!T lockConnection() { return LockedConnection!T(); } + + /// See: $(LINK http://vibed.org/api/vibe.core.connectionpool/ConnectionPool.maxConcurrency) + uint maxConcurrency; + + /// See: $(LINK https://github.com/vibe-d/vibe-core/blob/24a83434e4c788ebb9859dfaecbe60ad0f6e9983/source/vibe/core/connectionpool.d#L113) + void removeUnused(scope void delegate(Connection conn) @safe nothrow disconnect_callback) + {} + } + + /++ + Vibe.d's + $(LINK2 http://vibed.org/api/vibe.core.connectionpool/LockedConnection, LockedConnection) + struct. + + Not actually included in module `mysql.pool`. Only listed here for + documentation purposes. For LockedConnection and it's documentation, see: + $(LINK http://vibed.org/api/vibe.core.connectionpool/LockedConnection) + +/ + struct LockedConnection(Connection) { Connection c; alias c this; } + } + + /++ + A lightweight convenience interface to a MySQL/MariaDB database using vibe.d's + $(LINK2 http://vibed.org/api/vibe.core.connectionpool/ConnectionPool, ConnectionPool). + + You have to include vibe.d in your project to be able to use this class. + If you don't want to, refer to `mysql.connection.Connection`. + + If, for any reason, this class doesn't suit your needs, it's easy to just + use vibe.d's $(LINK2 http://vibed.org/api/vibe.core.connectionpool/ConnectionPool, ConnectionPool) + directly. Simply provide it with a delegate that creates a new `mysql.connection.Connection` + and does any other custom processing if needed. + +/ + class MySQLPoolImpl(bool isSafe) + { + private + { + string m_host; + string m_user; + string m_password; + string m_database; + ushort m_port; + SvrCapFlags m_capFlags; + static if(isSafe) + alias NewConnectionDelegate = void delegate(Connection) @safe; + else + alias NewConnectionDelegate = void delegate(Connection) @system; + NewConnectionDelegate m_onNewConnection; + ConnectionPool!Connection m_pool; + PreparedRegistrations!PreparedInfo preparedRegistrations; + + struct PreparedInfo + { + bool queuedForRelease = false; + } + + } + + /// Sets up a connection pool with the provided connection settings. + /// + /// The optional `onNewConnection` param allows you to set a callback + /// which will be run every time a new connection is created. + this(string host, string user, string password, string database, + ushort port = 3306, uint maxConcurrent = (uint).max, + SvrCapFlags capFlags = defaultClientFlags, + NewConnectionDelegate onNewConnection = null) + { + m_host = host; + m_user = user; + m_password = password; + m_database = database; + m_port = port; + m_capFlags = capFlags; + m_onNewConnection = onNewConnection; + m_pool = new ConnectionPool!Connection(&createConnection); + } + + ///ditto + this(string host, string user, string password, string database, + ushort port, SvrCapFlags capFlags, NewConnectionDelegate onNewConnection = null) + { + this(host, user, password, database, port, (uint).max, capFlags, onNewConnection); + } + + ///ditto + this(string host, string user, string password, string database, + ushort port, NewConnectionDelegate onNewConnection) + { + this(host, user, password, database, port, (uint).max, defaultClientFlags, onNewConnection); + } + + ///ditto + this(string connStr, uint maxConcurrent = (uint).max, SvrCapFlags capFlags = defaultClientFlags, + NewConnectionDelegate onNewConnection = null) + { + auto parts = Connection.parseConnectionString(connStr); + this(parts[0], parts[1], parts[2], parts[3], to!ushort(parts[4]), capFlags, onNewConnection); + } + + ///ditto + this(string connStr, SvrCapFlags capFlags, NewConnectionDelegate onNewConnection = null) + { + this(connStr, (uint).max, capFlags, onNewConnection); + } + + ///ditto + this(string connStr, NewConnectionDelegate onNewConnection) + { + this(connStr, (uint).max, defaultClientFlags, onNewConnection); + } + + /++ + Obtain a connection. If one isn't available, a new one will be created. + + The connection returned is actually a `LockedConnection!Connection`, + but it uses `alias this`, and so can be used just like a Connection. + (See vibe.d's + $(LINK2 http://vibed.org/api/vibe.core.connectionpool/LockedConnection, LockedConnection documentation).) + + No other fiber will be given this `mysql.connection.Connection` as long as your fiber still holds it. + + There is no need to close, release or unlock this connection. It is + reference-counted and will automatically be returned to the pool once + your fiber is done with it. + + If you have passed any prepared statements to `autoRegister` + or `autoRelease`, then those statements will automatically be + registered/released on the connection. (Currently, this automatic + register/release may actually occur upon the first command sent via + the connection.) + +/ + static if(isSafe) + LockedConnection!Connection lockConnection() @safe + { + return lockConnectionImpl(); + } + else + LockedConnection!Connection lockConnection() + { + return lockConnectionImpl(); + } + + // the implementation we want to imply attributes + private final lockConnectionImpl() + { + auto conn = m_pool.lockConnection(); + if(conn.closed) + conn.reconnect(); + + applyAuto(conn); + return conn; + } + + /// Applies any `autoRegister`/`autoRelease` settings to a connection, + /// if necessary. + private void applyAuto(T)(T conn) + { + foreach(sql, info; preparedRegistrations.directLookup) + { + auto registeredOnPool = !info.queuedForRelease; + auto registeredOnConnection = conn.isRegistered(sql); + + if(registeredOnPool && !registeredOnConnection) // Need to register? + conn.register(sql); + else if(!registeredOnPool && registeredOnConnection) // Need to release? + conn.release(sql); + } + } + + private Connection createConnection() + { + auto conn = new Connection(m_host, m_user, m_password, m_database, m_port, m_capFlags); + + if(m_onNewConnection) + m_onNewConnection(conn); + + return conn; + } + + /// Get/set a callback delegate to be run every time a new connection + /// is created. + @property void onNewConnection(NewConnectionDelegate onNewConnection) @safe + { + m_onNewConnection = onNewConnection; + } + + ///ditto + @property NewConnectionDelegate onNewConnection() @safe + { + return m_onNewConnection; + } + + @("onNewConnection") + debug(MYSQLN_TESTS) + unittest + { + auto count = 0; + void callback(Connection conn) + { + count++; + } + + // Test getting/setting + auto poolA = new MySQLPoolImpl(testConnectionStr, &callback); + auto poolB = new MySQLPoolImpl(testConnectionStr); + auto poolNoCallback = new MySQLPoolImpl(testConnectionStr); + + assert(poolA.onNewConnection == &callback); + assert(poolB.onNewConnection is null); + assert(poolNoCallback.onNewConnection is null); + + poolB.onNewConnection = &callback; + assert(poolB.onNewConnection == &callback); + assert(count == 0); + + // Ensure callback is called + { + auto connA = poolA.lockConnection(); + assert(!connA.closed); + assert(count == 1); + + auto connB = poolB.lockConnection(); + assert(!connB.closed); + assert(count == 2); + } + + // Ensure works with no callback + { + auto oldCount = count; + auto poolC = new MySQLPoolImpl(testConnectionStr); + auto connC = poolC.lockConnection(); + assert(!connC.closed); + assert(count == oldCount); + } + } + + /++ + Forwards to vibe.d's + $(LINK2 http://vibed.org/api/vibe.core.connectionpool/ConnectionPool.maxConcurrency, ConnectionPool.maxConcurrency) + +/ + @property uint maxConcurrency() @safe + { + return m_pool.maxConcurrency; + } + + ///ditto + @property void maxConcurrency(uint maxConcurrent) @safe + { + m_pool.maxConcurrency = maxConcurrent; + } + + /++ + Set a prepared statement to be automatically registered on all + connections received from this pool. + + This also clears any `autoRelease` which may have been set for this statement. + + Calling this is not strictly necessary, as a prepared statement will + automatically be registered upon its first use on any `Connection`. + This is provided for those who prefer eager registration over lazy + for performance reasons. + + Once this has been called, obtaining a connection via `lockConnection` + will automatically register the prepared statement on the connection + if it isn't already registered on the connection. This single + registration safely persists after the connection is reclaimed by the + pool and locked again by another Vibe.d task. + + Note, due to the way Vibe.d works, it is not possible to eagerly + register or release a statement on all connections already sitting + in the pool. This can only be done when locking a connection. + + You can stop the pool from continuing to auto-register the statement + by calling either `autoRelease` or `clearAuto`. + +/ + void autoRegister(SafePrepared prepared) @safe + { + autoRegister(prepared.sql); + } + + ///ditto + void autoRegister(const(char[]) sql) @safe + { + preparedRegistrations.registerIfNeeded(sql, (sql) => PreparedInfo()); + } + + /++ + Set a prepared statement to be automatically released from all + connections received from this pool. + + This also clears any `autoRegister` which may have been set for this statement. + + Calling this is not strictly necessary. The server considers prepared + statements to be per-connection, so they'll go away when the connection + closes anyway. This is provided in case direct control is actually needed. + + Once this has been called, obtaining a connection via `lockConnection` + will automatically release the prepared statement from the connection + if it isn't already releases from the connection. + + Note, due to the way Vibe.d works, it is not possible to eagerly + register or release a statement on all connections already sitting + in the pool. This can only be done when locking a connection. + + You can stop the pool from continuing to auto-release the statement + by calling either `autoRegister` or `clearAuto`. + +/ + void autoRelease(SafePrepared prepared) @safe + { + autoRelease(prepared.sql); + } + + ///ditto + void autoRelease(const(char[]) sql) @safe + { + preparedRegistrations.queueForRelease(sql); + } + + /// Is the given statement set to be automatically registered on all + /// connections obtained from this connection pool? + bool isAutoRegistered(Prepared prepared) @safe + { + return isAutoRegistered(prepared.sql); + } + ///ditto + bool isAutoRegistered(const(char[]) sql) @safe + { + return isAutoRegistered(preparedRegistrations[sql]); + } + ///ditto + package bool isAutoRegistered(Nullable!PreparedInfo info) @safe + { + return info.isNull || !info.get.queuedForRelease; + } + + /// Is the given statement set to be automatically released on all + /// connections obtained from this connection pool? + bool isAutoReleased(Prepared prepared) @safe + { + return isAutoReleased(prepared.sql); + } + ///ditto + bool isAutoReleased(const(char[]) sql) @safe + { + return isAutoReleased(preparedRegistrations[sql]); + } + ///ditto + package bool isAutoReleased(Nullable!PreparedInfo info) @safe + { + return info.isNull || info.get.queuedForRelease; + } + + /++ + Is the given statement set for NEITHER auto-register + NOR auto-release on connections obtained from + this connection pool? + + Equivalent to `!isAutoRegistered && !isAutoReleased`. + +/ + bool isAutoCleared(Prepared prepared) @safe + { + return isAutoCleared(prepared.sql); + } + ///ditto + bool isAutoCleared(const(char[]) sql) @safe + { + return isAutoCleared(preparedRegistrations[sql]); + } + ///ditto + package bool isAutoCleared(Nullable!PreparedInfo info) @safe + { + return info.isNull; + } + + /++ + Removes any `autoRegister` or `autoRelease` which may have been set + for this prepared statement. + + Does nothing if the statement has not been set for auto-register or auto-release. + + This releases any relevent memory for potential garbage collection. + +/ + void clearAuto(SafePrepared prepared) @safe + { + return clearAuto(prepared.sql); + } + ///ditto + void clearAuto(const(char[]) sql) @safe + { + preparedRegistrations.directLookup.remove(sql); + } + + /++ + Removes ALL prepared statement `autoRegister` and `autoRelease` which have been set. + + This releases all relevent memory for potential garbage collection. + +/ + void clearAllRegistrations() @safe + { + preparedRegistrations.clear(); + } + + version(HaveCleanupFunction) + { + /++ + Removes all unused connections from the pool. This can + be used to clean up before exiting the program to + ensure the event core driver can be properly shut down. + + Note: this is only available if vibe-core 1.7.0 or later is being + used. + +/ + void removeUnusedConnections() @safe + { + // Note: we squelch all exceptions here, because vibe-core + // requires the function be nothrow, and because an exception + // thrown while closing is probably not important enough to + // interrupt cleanup. + m_pool.removeUnused((conn) @trusted nothrow { + try { + conn.close(); + } catch(Exception) {} + }); + } + } + } + + @("registration") + debug(MYSQLN_TESTS) + unittest + { + static void doit(bool isSafe)() + { + static if(isSafe) + import mysql.safe.commands; + else + import mysql.unsafe.commands; + alias MySQLPool = MySQLPoolImpl!isSafe; + auto pool = new MySQLPool(testConnectionStr); + + // Setup + Connection cn = pool.lockConnection(); + cn.exec("DROP TABLE IF EXISTS `poolRegistration`"); + cn.exec("CREATE TABLE `poolRegistration` ( + `data` LONGBLOB + ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + immutable sql = "SELECT * from `poolRegistration`"; + //auto cn2 = pool.lockConnection(); // Seems to return the same connection as `cn` + auto cn2 = pool.createConnection(); + pool.applyAuto(cn2); + assert(cn !is cn2); + + // Tests: + // Initial + assert(pool.isAutoCleared(sql)); + assert(pool.isAutoRegistered(sql)); + assert(pool.isAutoReleased(sql)); + assert(!cn.isRegistered(sql)); + assert(!cn2.isRegistered(sql)); + + // Register on connection #1 + auto prepared = cn.prepare(sql); + { + assert(pool.isAutoCleared(sql)); + assert(pool.isAutoRegistered(sql)); + assert(pool.isAutoReleased(sql)); + assert(cn.isRegistered(sql)); + assert(!cn2.isRegistered(sql)); + + //auto cn3 = pool.lockConnection(); // Seems to return the same connection as `cn` + auto cn3 = pool.createConnection(); + pool.applyAuto(cn3); + assert(!cn3.isRegistered(sql)); + } + + // autoRegister + pool.autoRegister(prepared); + { + assert(!pool.isAutoCleared(sql)); + assert(pool.isAutoRegistered(sql)); + assert(!pool.isAutoReleased(sql)); + assert(cn.isRegistered(sql)); + assert(!cn2.isRegistered(sql)); + + //auto cn3 = pool.lockConnection(); // Seems to return the *same* connection as `cn` + auto cn3 = pool.createConnection(); + pool.applyAuto(cn3); + assert(cn3.isRegistered(sql)); + } + + // autoRelease + pool.autoRelease(prepared); + { + assert(!pool.isAutoCleared(sql)); + assert(!pool.isAutoRegistered(sql)); + assert(pool.isAutoReleased(sql)); + assert(cn.isRegistered(sql)); + assert(!cn2.isRegistered(sql)); + + //auto cn3 = pool.lockConnection(); // Seems to return the same connection as `cn` + auto cn3 = pool.createConnection(); + pool.applyAuto(cn3); + assert(!cn3.isRegistered(sql)); + } + + // clearAuto + pool.clearAuto(prepared); + { + assert(pool.isAutoCleared(sql)); + assert(pool.isAutoRegistered(sql)); + assert(pool.isAutoReleased(sql)); + assert(cn.isRegistered(sql)); + assert(!cn2.isRegistered(sql)); + + //auto cn3 = pool.lockConnection(); // Seems to return the same connection as `cn` + auto cn3 = pool.createConnection(); + pool.applyAuto(cn3); + assert(!cn3.isRegistered(sql)); + } + } + + // run tests for both safe and unsafe options. + () @safe {doit!true(); }(); + doit!false(); + } + + @("closedConnection") // "cct" + debug(MYSQLN_TESTS) + unittest + { + static void doit(bool isSafe)() + { + static if(isSafe) + import mysql.safe.commands; + else + import mysql.unsafe.commands; + alias MySQLPool = MySQLPoolImpl!isSafe; + MySQLPool cctPool; + int cctCount=0; + + void cctStart() + { + import std.array; + import mysql.commands; + + cctPool = new MySQLPool(testConnectionStr); + cctPool.onNewConnection = (Connection conn) { cctCount++; }; + assert(cctCount == 0); + + auto cn = cctPool.lockConnection(); + assert(!cn.closed); + cn.close(); + assert(cn.closed); + assert(cctCount == 1); + } + + { + cctStart(); + assert(cctCount == 1); + + auto cn = cctPool.lockConnection(); + assert(cctCount == 1); + assert(!cn.closed); + } + } + + // run tests for both safe and unsafe options. + () @safe {doit!true(); }(); + doit!false(); + } +} diff --git a/source/mysql/internal/prepared.d b/source/mysql/internal/prepared.d new file mode 100644 index 00000000..867526be --- /dev/null +++ b/source/mysql/internal/prepared.d @@ -0,0 +1,819 @@ +/// Use a DB via SQL prepared statements. +module mysql.internal.prepared; + +import std.exception; +import std.range; +import std.traits; +import std.typecons; +import std.variant; + +import mysql.exceptions; +import mysql.protocol.comms; +import mysql.protocol.constants; +import mysql.protocol.packets; +import mysql.types; +import mysql.internal.result; +import mysql.safe.commands : ColumnSpecialization, CSN; +debug(MYSQLN_TESTS) + import mysql.test.common; + +/++ +A struct to represent specializations of prepared statement parameters. + +If you need to send large objects to the database it might be convenient to +send them in pieces. The `chunkSize` and `chunkDelegate` variables allow for this. +If both are provided then the corresponding column will be populated by calling the delegate repeatedly. +The source should fill the indicated slice with data and arrange for the delegate to +return the length of the data supplied (in bytes). If that is less than the `chunkSize` +then the chunk will be assumed to be the last one. ++/ +struct ParameterSpecializationImpl(bool isSafe) +{ + import mysql.protocol.constants; + + size_t pIndex; //parameter number 0 - number of params-1 + SQLType type = SQLType.INFER_FROM_D_TYPE; + uint chunkSize; /// In bytes + static if(isSafe) + uint delegate(ubyte[]) @safe chunkDelegate; + else + uint delegate(ubyte[]) @system chunkDelegate; +} + +/// ditto +alias SafeParameterSpecialization = ParameterSpecializationImpl!true; +/// ditto +alias UnsafeParameterSpecialization = ParameterSpecializationImpl!false; +/// ditto +alias SPSN = SafeParameterSpecialization; +/// ditto +alias UPSN = UnsafeParameterSpecialization; + +@("paramSpecial") +debug(MYSQLN_TESTS) +unittest +{ + import std.array; + import std.range; + import mysql.safe.connection; + import mysql.safe.commands; + import mysql.test.common; + mixin(scopedCn); + + // Setup + cn.exec("DROP TABLE IF EXISTS `paramSpecial`"); + cn.exec("CREATE TABLE `paramSpecial` ( + `data` LONGBLOB + ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + + immutable totalSize = 1000; // Deliberately not a multiple of chunkSize below + auto alph = cast(const(ubyte)[]) "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + auto data = alph.cycle.take(totalSize).array; + + int chunkSize; + const(ubyte)[] dataToSend; + bool finished; + uint sender(ubyte[] chunk) + { + assert(!finished); + assert(chunk.length == chunkSize); + + if(dataToSend.length < chunkSize) + { + auto actualSize = cast(uint) dataToSend.length; + chunk[0..actualSize] = dataToSend[]; + finished = true; + dataToSend.length = 0; + return actualSize; + } + else + { + chunk[] = dataToSend[0..chunkSize]; + dataToSend = dataToSend[chunkSize..$]; + return chunkSize; + } + } + + immutable selectSQL = "SELECT `data` FROM `paramSpecial`"; + + // Sanity check + cn.exec("INSERT INTO `paramSpecial` VALUES (\""~(cast(string)data)~"\")"); + auto value = cn.queryValue(selectSQL); + assert(!value.isNull); + assert(value.get == data); + + { + // Clear table + cn.exec("DELETE FROM `paramSpecial`"); + value = cn.queryValue(selectSQL); // Ensure deleted + assert(value.isNull); + + // Test: totalSize as a multiple of chunkSize + chunkSize = 100; + assert(cast(int)(totalSize / chunkSize) * chunkSize == totalSize); + auto paramSpecial = SafeParameterSpecialization(0, SQLType.INFER_FROM_D_TYPE, chunkSize, &sender); + + finished = false; + dataToSend = data; + auto prepared = cn.prepare("INSERT INTO `paramSpecial` VALUES (?)"); + prepared.setArg(0, cast(ubyte[])[], paramSpecial); + assert(cn.exec(prepared) == 1); + value = cn.queryValue(selectSQL); + assert(!value.isNull); + assert(value.get == data); + } + + { + // Clear table + cn.exec("DELETE FROM `paramSpecial`"); + value = cn.queryValue(selectSQL); // Ensure deleted + assert(value.isNull); + + // Test: totalSize as a non-multiple of chunkSize + chunkSize = 64; + assert(cast(int)(totalSize / chunkSize) * chunkSize != totalSize); + auto paramSpecial = SafeParameterSpecialization(0, SQLType.INFER_FROM_D_TYPE, chunkSize, &sender); + + finished = false; + dataToSend = data; + auto prepared = cn.prepare("INSERT INTO `paramSpecial` VALUES (?)"); + prepared.setArg(0, cast(ubyte[])[], paramSpecial); + assert(cn.exec(prepared) == 1); + value = cn.queryValue(selectSQL); + assert(!value.isNull); + assert(value.get == data); + } +} + +/++ +Encapsulation of a prepared statement. + +Create this via the function `mysql.connection.prepare`. Set your arguments (if any) via +the functions provided, and then run the statement by passing it to +`mysql.commands.exec`/`mysql.commands.query`/etc in place of the sql string parameter. + +Commands that are expected to return a result set - queries - have distinctive +methods that are enforced. That is it will be an error to call such a method +with an SQL command that does not produce a result set. So for commands like +SELECT, use the `mysql.commands.query` functions. For other commands, like +INSERT/UPDATE/CREATE/etc, use `mysql.commands.exec`. ++/ +struct SafePrepared +{ + @safe: +private: + const(char)[] _sql; + +package(mysql): + ushort _numParams; /// Number of parameters this prepared statement takes + PreparedStmtHeaders _headers; + MySQLVal[] _inParams; + SPSN[] _psa; + CSN[] _columnSpecials; + ulong _lastInsertID; + + ExecQueryImplInfo getExecQueryImplInfo(uint statementId) + { + return ExecQueryImplInfo(true, null, statementId, _headers, _inParams, _psa); + } + +public: + /++ + Constructor. You probably want `mysql.connection.prepare` instead of this. + + Call `mysqln.connection.prepare` instead of this, unless you are creating + your own transport bypassing `mysql.connection.Connection` entirely. + The prepared statement must be registered on the server BEFORE this is + called (which `mysqln.connection.prepare` does). + + Internally, the result of a successful outcome will be a statement handle - an ID - + for the prepared statement, a count of the parameters required for + execution of the statement, and a count of the columns that will be present + in any result set that the command generates. + + The server will then proceed to send prepared statement headers, + including parameter descriptions, and result set field descriptions, + followed by an EOF packet. + +/ + this(const(char[]) sql, PreparedStmtHeaders headers, ushort numParams) + { + this._sql = sql; + this._headers = headers; + this._numParams = numParams; + _inParams.length = numParams; + _psa.length = numParams; + } + + /++ + Prepared statement parameter setter. + + The value may, but doesn't have to be, wrapped in a MySQLVal. If so, + null is handled correctly. + + The value may, but doesn't have to be, a pointer to the desired value. + + The value may, but doesn't have to be, wrapped in a Nullable!T. If so, + null is handled correctly. + + The value can be null. + + Parameter specializations (ie, for chunked transfer) can be added if required. + If you wish to use chunked transfer (via `psn`), note that you must supply + a dummy value for `val` that's typed `ubyte[]`. For example: `cast(ubyte[])[]`. + + Type_Mappings: $(TYPE_MAPPINGS) + + Params: index = The zero based index + +/ + void setArg(T)(size_t index, T val, SafeParameterSpecialization psn = SPSN.init) + if(!isInstanceOf!(Nullable, T) && !is(T == Variant)) + { + // Now in theory we should be able to check the parameter type here, since the + // protocol is supposed to send us type information for the parameters, but this + // capability seems to be broken. This assertion is supported by the fact that + // the same information is not available via the MySQL C API either. It is up + // to the programmer to ensure that appropriate type information is embodied + // in the variant array, or provided explicitly. This sucks, but short of + // having a client side SQL parser I don't see what can be done. + + enforce!MYX(index < _numParams, "Parameter index out of range."); + + _inParams[index] = val; + psn.pIndex = index; + _psa[index] = psn; + } + + ///ditto + void setArg(T)(size_t index, Nullable!T val, SafeParameterSpecialization psn = SPSN.init) + { + if(val.isNull) + setArg(index, null, psn); + else + setArg(index, val.get(), psn); + } + + @("setArg-typeMods") + debug(MYSQLN_TESTS) + unittest + { + import mysql.safe.commands; + import mysql.test.common; + mixin(scopedCn); + + // Setup + cn.exec("DROP TABLE IF EXISTS `setArg-typeMods`"); + cn.exec("CREATE TABLE `setArg-typeMods` ( + `i` INTEGER + ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + + auto insertSQL = "INSERT INTO `setArg-typeMods` VALUES (?)"; + + // Sanity check + { + int i = 111; + assert(cn.exec(insertSQL, i) == 1); + auto value = cn.queryValue("SELECT `i` FROM `setArg-typeMods`"); + assert(!value.isNull); + assert(value.get == i); + } + + // Test const(int) + { + const(int) i = 112; + assert(cn.exec(insertSQL, i) == 1); + } + + // Test immutable(int) + { + immutable(int) i = 113; + assert(cn.exec(insertSQL, i) == 1); + } + + // Note: Variant doesn't seem to support + // `shared(T)` or `shared(const(T)`. Only `shared(immutable(T))`. + + // Further note, shared immutable(int) is really + // immutable(int). This test is a duplicate, so removed. + // Test shared immutable(int) + /*{ + shared immutable(int) i = 113; + assert(cn.exec(insertSQL, i) == 1); + }*/ + } + + /++ + Bind a tuple of D variables to the parameters of a prepared statement. + + You can use this method to bind a set of variables if you don't need any specialization, + that is chunked transfer is not neccessary. + + The tuple must match the required number of parameters, and it is the programmer's + responsibility to ensure that they are of appropriate types. + + Type_Mappings: $(TYPE_MAPPINGS) + +/ + void setArgs(T...)(T args) + if(T.length == 0 || (!is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]))) + { + enforce!MYX(args.length == _numParams, "Argument list supplied does not match the number of parameters."); + + foreach (size_t i, arg; args) + setArg(i, arg); + } + + /++ + Bind a MySQLVal[] as the parameters of a prepared statement. + + You can use this method to bind a set of variables in MySQLVal form to + the parameters of a prepared statement. + + Parameter specializations (ie, for chunked transfer) can be added if required. + If you wish to use chunked transfer (via `psn`), note that you must supply + a dummy value for `val` that's typed `ubyte[]`. For example: `cast(ubyte[])[]`. + + This method could be + used to add records from a data entry form along the lines of + ------------ + auto stmt = conn.prepare("INSERT INTO `table42` VALUES(?, ?, ?)"); + DataRecord dr; // Some data input facility + ulong ra; + do + { + dr.get(); + stmt.setArgs(dr("Name"), dr("City"), dr("Whatever")); + ulong rowsAffected = conn.exec(stmt); + } while(!dr.done); + ------------ + + Type_Mappings: $(TYPE_MAPPINGS) + + Params: + args = External list of MySQLVal to be used as parameters + psnList = Any required specializations + +/ + void setArgs(MySQLVal[] args, SafeParameterSpecialization[] psnList=null) + { + enforce!MYX(args.length == _numParams, "Param count supplied does not match prepared statement"); + _inParams[] = args[]; + if (psnList !is null) + { + foreach (psn; psnList) + _psa[psn.pIndex] = psn; + } + } + + /++ + Prepared statement parameter getter. + + Type_Mappings: $(TYPE_MAPPINGS) + + Params: index = The zero based index + + Note: The type of getArg's return is now MySQLVal. As a stop-gap measure, + mysql-native provides the vGetArg version. This version will be removed + in a future update. + +/ + MySQLVal getArg(size_t index) + { + enforce!MYX(index < _numParams, "Parameter index out of range."); + return _inParams[index]; + } + + /// ditto + Variant vGetArg(size_t index) @system + { + // convert to Variant. + return getArg(index).asVariant; + } + + /++ + Sets a prepared statement parameter to NULL. + + This is here mainly for legacy reasons. You can set a field to null + simply by saying `prepared.setArg(index, null);` + + Type_Mappings: $(TYPE_MAPPINGS) + + Params: index = The zero based index + +/ + void setNullArg(size_t index) + { + setArg(index, null); + } + + /// Gets the SQL command for this prepared statement. + const(char)[] sql() pure const + { + return _sql; + } + + @("setNullArg") + debug(MYSQLN_TESTS) + unittest + { + import mysql.safe.connection; + import mysql.test.common; + import mysql.safe.commands; + mixin(scopedCn); + + cn.exec("DROP TABLE IF EXISTS `setNullArg`"); + cn.exec("CREATE TABLE `setNullArg` ( + `val` INTEGER + ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + + immutable insertSQL = "INSERT INTO `setNullArg` VALUES (?)"; + immutable selectSQL = "SELECT * FROM `setNullArg`"; + auto preparedInsert = cn.prepare(insertSQL); + assert(preparedInsert.sql == insertSQL); + SafeRow[] rs; + + { + Nullable!int nullableInt; + nullableInt.nullify(); + preparedInsert.setArg(0, nullableInt); + assert(preparedInsert.getArg(0).kind == MySQLVal.Kind.Null); + nullableInt = 7; + preparedInsert.setArg(0, nullableInt); + assert(preparedInsert.getArg(0) == 7); + + nullableInt.nullify(); + preparedInsert.setArgs(nullableInt); + assert(preparedInsert.getArg(0).kind == MySQLVal.Kind.Null); + nullableInt = 7; + preparedInsert.setArgs(nullableInt); + assert(preparedInsert.getArg(0) == 7); + } + + preparedInsert.setArg(0, 5); + cn.exec(preparedInsert); + rs = cn.query(selectSQL).array; + assert(rs.length == 1); + assert(rs[0][0] == 5); + + preparedInsert.setArg(0, null); + cn.exec(preparedInsert); + rs = cn.query(selectSQL).array; + assert(rs.length == 2); + assert(rs[0][0] == 5); + assert(rs[1].isNull(0)); + assert(rs[1][0].kind == MySQLVal.Kind.Null); + + preparedInsert.setArg(0, MySQLVal(null)); + cn.exec(preparedInsert); + rs = cn.query(selectSQL).array; + assert(rs.length == 3); + assert(rs[0][0] == 5); + assert(rs[1].isNull(0)); + assert(rs[2].isNull(0)); + assert(rs[1][0].kind == MySQLVal.Kind.Null); + assert(rs[2][0].kind == MySQLVal.Kind.Null); + } + + /// Gets the number of arguments this prepared statement expects to be passed in. + @property ushort numArgs() pure const nothrow + { + return _numParams; + } + + /// After a command that inserted a row into a table with an auto-increment + /// ID column, this method allows you to retrieve the last insert ID generated + /// from this prepared statement. + @property ulong lastInsertID() pure const nothrow { return _lastInsertID; } + + @("lastInsertID") + debug(MYSQLN_TESTS) + unittest + { + import mysql.connection; + import mysql.safe.commands; + mixin(scopedCn); + cn.exec("DROP TABLE IF EXISTS `testPreparedLastInsertID`"); + cn.exec("CREATE TABLE `testPreparedLastInsertID` ( + `a` INTEGER NOT NULL AUTO_INCREMENT, + PRIMARY KEY (a) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + + auto stmt = cn.prepare("INSERT INTO `testPreparedLastInsertID` VALUES()"); + cn.exec(stmt); + assert(stmt.lastInsertID == 1); + cn.exec(stmt); + assert(stmt.lastInsertID == 2); + cn.exec(stmt); + assert(stmt.lastInsertID == 3); + } + + /// Gets the prepared header's field descriptions. + @property FieldDescription[] preparedFieldDescriptions() pure { return _headers.fieldDescriptions; } + + /// Gets the prepared header's param descriptions. + @property ParamDescription[] preparedParamDescriptions() pure { return _headers.paramDescriptions; } + + /// Get/set the column specializations. + @property ColumnSpecialization[] columnSpecials() pure { return _columnSpecials; } + + ///ditto + @property void columnSpecials(ColumnSpecialization[] csa) pure { _columnSpecials = csa; } +} + +// unsafe wrapper +struct UnsafePrepared +{ + SafePrepared _safe; + + private this(SafePrepared sp) @safe + { + _safe = sp; + } + + this(const(char[]) sql, PreparedStmtHeaders headers, ushort numParams) @safe + { + _safe = SafePrepared(sql, headers, numParams); + } + + // redefine all functions that deal with unsafe types + void setArg(T)(size_t index, T val, UnsafeParameterSpecialization psn = UPSN.init) @system + if(!is(T == Variant)) + { + _safe.setArg(index, val, cast(SafeParameterSpecialization)psn); + } + + void setArg(size_t index, Variant val, UnsafeParameterSpecialization psn = UPSN.init) @system + { + _safe.setArg(index, _toVal(val), cast(SPSN)psn); + } + + // unfortunately, we need to redefine this here + void setArgs(T...)(T args) + if(T.length == 0 || (!is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]))) + { + _safe.setArgs(args); + } + + void setArgs(Variant[] args, UnsafeParameterSpecialization[] psnList=null) @system + { + enforce!MYX(args.length == _numParams, "Param count supplied does not match prepared statement"); + foreach(i, ref arg; args) + _safe.setArg(i, _toVal(arg)); + if (psnList !is null) + { + foreach (psn; psnList) + _safe._psa[psn.pIndex] = cast(SPSN)psn; + } + } + + Variant getArg(size_t index) @system + { + return _safe.getArg(index).asVariant; + } + + alias _safe this; +} + +UnsafePrepared unsafe(SafePrepared p) @safe +{ + return UnsafePrepared(p); +} + +SafePrepared safe(UnsafePrepared p) @safe +{ + return p._safe; +} + + + +/// Template constraint for `PreparedRegistrations` +private enum isPreparedRegistrationsPayload(Payload) = + __traits(compiles, (){ + static assert(Payload.init.queuedForRelease == false); + Payload p; + p.queuedForRelease = true; + }); + +/++ +Common functionality for recordkeeping of prepared statement registration +and queueing for unregister. + +Used by `Connection` and `MySQLPool`. + +Templated on payload type. The payload should be an aggregate that includes +the field: `bool queuedForRelease = false;` + +Allowing access to `directLookup` from other parts of mysql-native IS intentional. +`PreparedRegistrations` isn't intended as 100% encapsulation, it's mainly just +to factor out common functionality needed by both `Connection` and `MySQLPool`. ++/ +package(mysql) struct PreparedRegistrations(Payload) + if( isPreparedRegistrationsPayload!Payload) +{ + @safe: + /++ + Lookup payload by sql string. + + Allowing access to `directLookup` from other parts of mysql-native IS intentional. + `PreparedRegistrations` isn't intended as 100% encapsulation, it's mainly just + to factor out common functionality needed by both `Connection` and `MySQLPool`. + +/ + Payload[const(char[])] directLookup; + + /// Returns null if not found + Nullable!Payload opIndex(const(char[]) sql) pure nothrow + { + Nullable!Payload result; + + auto pInfo = sql in directLookup; + if(pInfo) + result = *pInfo; + + return result; + } + + /// Set `queuedForRelease` flag for a statement in `directLookup`. + /// Does nothing if statement not in `directLookup`. + private void setQueuedForRelease(const(char[]) sql, bool value) + { + if(auto pInfo = sql in directLookup) + { + pInfo.queuedForRelease = value; + directLookup[sql] = *pInfo; + } + } + + /// Queue a prepared statement for release. + void queueForRelease(const(char[]) sql) + { + setQueuedForRelease(sql, true); + } + + /// Remove a statement from the queue to be released. + void unqueueForRelease(const(char[]) sql) + { + setQueuedForRelease(sql, false); + } + + /// Queues all prepared statements for release. + void queueAllForRelease() + { + foreach(sql, info; directLookup) + queueForRelease(sql); + } + + // Note: AA.clear does not invalidate any keys or values. In fact, it + // should really be safe/trusted, but is not. Therefore, we mark this + // as trusted. + /// Eliminate all records of both registered AND queued-for-release statements. + void clear() @trusted + { + static if(__traits(compiles, (){ int[int] aa; aa.clear(); })) + directLookup.clear(); + else + directLookup = null; + } + + /// If already registered, simply returns the cached Payload. + Payload registerIfNeeded(const(char[]) sql, Payload delegate(const(char[])) @safe doRegister) + out(info) + { + // I'm confident this can't currently happen, but + // let's make sure that doesn't change. + assert(!info.queuedForRelease); + } + body + { + if(auto pInfo = sql in directLookup) + { + // The statement is registered. It may, or may not, be queued + // for release. Either way, all we need to do is make sure it's + // un-queued and then return. + pInfo.queuedForRelease = false; + return *pInfo; + } + + auto info = doRegister(sql); + directLookup[sql] = info; + + return info; + } +} + +// Test PreparedRegistrations +debug(MYSQLN_TESTS) +{ + // Test template constraint + struct TestPreparedRegistrationsBad1 { } + struct TestPreparedRegistrationsBad2 { bool foo = false; } + struct TestPreparedRegistrationsBad3 { int queuedForRelease = 1; } + struct TestPreparedRegistrationsBad4 { bool queuedForRelease = true; } + struct TestPreparedRegistrationsGood1 { bool queuedForRelease = false; } + struct TestPreparedRegistrationsGood2 { bool queuedForRelease = false; const(char)[] id; } + + static assert(!isPreparedRegistrationsPayload!int); + static assert(!isPreparedRegistrationsPayload!bool); + static assert(!isPreparedRegistrationsPayload!TestPreparedRegistrationsBad1); + static assert(!isPreparedRegistrationsPayload!TestPreparedRegistrationsBad2); + static assert(!isPreparedRegistrationsPayload!TestPreparedRegistrationsBad3); + static assert(!isPreparedRegistrationsPayload!TestPreparedRegistrationsBad4); + //static assert(isPreparedRegistrationsPayload!TestPreparedRegistrationsGood1); + //static assert(isPreparedRegistrationsPayload!TestPreparedRegistrationsGood2); + PreparedRegistrations!TestPreparedRegistrationsGood1 testPreparedRegistrationsGood1; + PreparedRegistrations!TestPreparedRegistrationsGood2 testPreparedRegistrationsGood2; + + @("PreparedRegistrations") + unittest + { + // Test init + PreparedRegistrations!TestPreparedRegistrationsGood2 pr; + assert(pr.directLookup.keys.length == 0); + + void resetData(bool isQueued1, bool isQueued2, bool isQueued3) + { + pr.directLookup["1"] = TestPreparedRegistrationsGood2(isQueued1, "1"); + pr.directLookup["2"] = TestPreparedRegistrationsGood2(isQueued2, "2"); + pr.directLookup["3"] = TestPreparedRegistrationsGood2(isQueued3, "3"); + assert(pr.directLookup.keys.length == 3); + } + + // Test resetData (sanity check) + resetData(false, true, false); + assert(pr.directLookup["1"] == TestPreparedRegistrationsGood2(false, "1")); + assert(pr.directLookup["2"] == TestPreparedRegistrationsGood2(true, "2")); + assert(pr.directLookup["3"] == TestPreparedRegistrationsGood2(false, "3")); + + // Test opIndex + resetData(false, true, false); + pr.directLookup["1"] = TestPreparedRegistrationsGood2(false, "1"); + pr.directLookup["2"] = TestPreparedRegistrationsGood2(true, "2"); + pr.directLookup["3"] = TestPreparedRegistrationsGood2(false, "3"); + assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); + assert(pr["2"] == TestPreparedRegistrationsGood2(true, "2")); + assert(pr["3"] == TestPreparedRegistrationsGood2(false, "3")); + assert(pr["4"].isNull); + + // Test queueForRelease + resetData(false, true, false); + pr.queueForRelease("2"); + assert(pr.directLookup.keys.length == 3); + assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); + assert(pr["2"] == TestPreparedRegistrationsGood2(true, "2")); + assert(pr["3"] == TestPreparedRegistrationsGood2(false, "3")); + + pr.queueForRelease("3"); + assert(pr.directLookup.keys.length == 3); + assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); + assert(pr["2"] == TestPreparedRegistrationsGood2(true, "2")); + assert(pr["3"] == TestPreparedRegistrationsGood2(true, "3")); + + pr.queueForRelease("4"); + assert(pr.directLookup.keys.length == 3); + assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); + assert(pr["2"] == TestPreparedRegistrationsGood2(true, "2")); + assert(pr["3"] == TestPreparedRegistrationsGood2(true, "3")); + + // Test unqueueForRelease + resetData(false, true, false); + pr.unqueueForRelease("1"); + assert(pr.directLookup.keys.length == 3); + assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); + assert(pr["2"] == TestPreparedRegistrationsGood2(true, "2")); + assert(pr["3"] == TestPreparedRegistrationsGood2(false, "3")); + + pr.unqueueForRelease("2"); + assert(pr.directLookup.keys.length == 3); + assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); + assert(pr["2"] == TestPreparedRegistrationsGood2(false, "2")); + assert(pr["3"] == TestPreparedRegistrationsGood2(false, "3")); + + pr.unqueueForRelease("4"); + assert(pr.directLookup.keys.length == 3); + assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); + assert(pr["2"] == TestPreparedRegistrationsGood2(false, "2")); + assert(pr["3"] == TestPreparedRegistrationsGood2(false, "3")); + + // Test queueAllForRelease + resetData(false, true, false); + pr.queueAllForRelease(); + assert(pr["1"] == TestPreparedRegistrationsGood2(true, "1")); + assert(pr["2"] == TestPreparedRegistrationsGood2(true, "2")); + assert(pr["3"] == TestPreparedRegistrationsGood2(true, "3")); + assert(pr["4"].isNull); + + // Test clear + resetData(false, true, false); + pr.clear(); + assert(pr.directLookup.keys.length == 0); + + // Test registerIfNeeded + auto doRegister(const(char[]) sql) { return TestPreparedRegistrationsGood2(false, sql); } + pr.registerIfNeeded("1", &doRegister); + assert(pr.directLookup.keys.length == 1); + assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); + + pr.registerIfNeeded("1", &doRegister); + assert(pr.directLookup.keys.length == 1); + assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); + + pr.registerIfNeeded("2", &doRegister); + assert(pr.directLookup.keys.length == 2); + assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); + assert(pr["2"] == TestPreparedRegistrationsGood2(false, "2")); + } +} diff --git a/source/mysql/internal/result.d b/source/mysql/internal/result.d new file mode 100644 index 00000000..39a20aea --- /dev/null +++ b/source/mysql/internal/result.d @@ -0,0 +1,391 @@ +/// Structures for data received: rows and result sets (ie, a range of rows). +module mysql.internal.result; + +import std.conv; +import std.exception; +import std.range; +import std.string; + +import mysql.connection; +import mysql.exceptions; +import mysql.protocol.comms; +import mysql.protocol.extra_types; +import mysql.protocol.packets; +public import mysql.types; +import std.typecons : Nullable; +import std.variant; + +/++ +A struct to represent a single row of a result set. + +Type_Mappings: $(TYPE_MAPPINGS) ++/ +/+ +The row struct is used for both 'traditional' and 'prepared' result sets. +It consists of parallel arrays of Variant and bool, with the bool array +indicating which of the result set columns are NULL. + +I have been agitating for some kind of null indicator that can be set for a +Variant without destroying its inherent type information. If this were the +case, then the bool array could disappear. ++/ +struct SafeRow +{ + import mysql.connection; + +package(mysql): + MySQLVal[] _values; // Temporarily "package" instead of "private" +private: + bool[] _nulls; + string[] _names; + +public: + @safe: + + /++ + A constructor to extract the column data from a row data packet. + + If the data for the row exceeds the server's maximum packet size, then several packets will be + sent for the row that taken together constitute a logical row data packet. The logic of the data + recovery for a Row attempts to minimize the quantity of data that is bufferred. Users can assist + in this by specifying chunked data transfer in cases where results sets can include long + column values. + + Type_Mappings: $(TYPE_MAPPINGS) + +/ + this(Connection con, ref ubyte[] packet, ResultSetHeaders rh, bool binary) + { + ctorRow(con, packet, rh, binary, _values, _nulls, _names); + } + + /++ + Simplify retrieval of a column value by index. + + To check for null, use Variant's `type` property: + `row[index].type == typeid(typeof(null))` + + Type_Mappings: $(TYPE_MAPPINGS) + + Params: i = the zero based index of the column whose value is required. + Returns: A Variant holding the column value. + +/ + ref inout(MySQLVal) opIndex(size_t i) inout + { + enforce!MYX(_nulls.length > 0, format("Cannot get column index %d. There are no columns", i)); + enforce!MYX(i < _nulls.length, format("Cannot get column index %d. The last available index is %d", i, _nulls.length-1)); + return _values[i]; + } + + /++ + Get the name of the column with specified index. + +/ + inout(string) getName(size_t index) inout + { + return _names[index]; + } + + @("getName") + debug(MYSQLN_TESTS) + unittest + { + import mysql.test.common; + import mysql.safe.commands; + mixin(scopedCn); + cn.exec("DROP TABLE IF EXISTS `row_getName`"); + cn.exec("CREATE TABLE `row_getName` (someValue INTEGER, another INTEGER) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + cn.exec("INSERT INTO `row_getName` VALUES (1, 2), (3, 4)"); + + enum sql = "SELECT another, someValue FROM `row_getName`"; + + auto rows = cn.query(sql).array; + assert(rows.length == 2); + assert(rows[0][0] == 2); + assert(rows[0][1] == 1); + assert(rows[0].getName(0) == "another"); + assert(rows[0].getName(1) == "someValue"); + assert(rows[1][0] == 4); + assert(rows[1][1] == 3); + assert(rows[1].getName(0) == "another"); + assert(rows[1].getName(1) == "someValue"); + } + + /++ + Check if a column in the result row was NULL + + Params: i = The zero based column index. + +/ + bool isNull(size_t i) const pure nothrow { return _nulls[i]; } + + /++ + Get the number of elements (columns) in this row. + +/ + @property size_t length() const pure nothrow { return _values.length; } + + ///ditto + alias opDollar = length; + + /++ + Move the content of the row into a compatible struct + + This method takes no account of NULL column values. If a column was NULL, + the corresponding Variant value would be unchanged in those cases. + + The method will throw if the type of the Variant is not implicitly + convertible to the corresponding struct member. + + Type_Mappings: $(TYPE_MAPPINGS) + + Params: + S = A struct type. + s = A ref instance of the type + +/ + void toStruct(S)(ref S s) if (is(S == struct)) + { + foreach (i, dummy; s.tupleof) + { + static if(__traits(hasMember, s.tupleof[i], "nullify") && + is(typeof(s.tupleof[i].nullify())) && is(typeof(s.tupleof[i].get))) + { + if(!_nulls[i]) + { + enforce!MYX(_values[i].convertsTo!(typeof(s.tupleof[i].get))(), + "At col "~to!string(i)~" the value is not implicitly convertible to the structure type"); + s.tupleof[i] = _values[i].get!(typeof(s.tupleof[i].get)); + } + else + s.tupleof[i].nullify(); + } + else + { + if(!_nulls[i]) + { + enforce!MYX(_values[i].convertsTo!(typeof(s.tupleof[i]))(), + "At col "~to!string(i)~" the value is not implicitly convertible to the structure type"); + s.tupleof[i] = _values[i].get!(typeof(s.tupleof[i])); + } + else + s.tupleof[i] = typeof(s.tupleof[i]).init; + } + } + } + + void show() + { + import std.stdio; + + writefln("%(%s, %)", _values); + } +} + +/// ditto +struct UnsafeRow +{ + SafeRow _safe; + alias _safe this; + Variant opIndex(size_t idx) { + return _safe[idx].asVariant; + } +} + +/// ditto +UnsafeRow unsafe(SafeRow r) @safe +{ + return UnsafeRow(r); +} + +/// ditto +Nullable!UnsafeRow unsafe(Nullable!SafeRow r) @safe +{ + if(r.isNull) + return Nullable!UnsafeRow(); + return Nullable!UnsafeRow(r.get.unsafe); +} + + +SafeRow safe(UnsafeRow r) @safe +{ + return r._safe; +} + + +Nullable!SafeRow safe(Nullable!UnsafeRow r) @safe +{ + if(r.isNull) + return Nullable!SafeRow(); + return Nullable!SafeRow(r.get.safe); +} + +/++ +An $(LINK2 http://dlang.org/phobos/std_range_primitives.html#isInputRange, input range) +of Row. + +This is returned by the `mysql.commands.query` functions. + +The rows are downloaded one-at-a-time, as you iterate the range. This allows +for low memory usage, and quick access to the results as they are downloaded. +This is especially ideal in case your query results in a large number of rows. + +However, because of that, this `ResultRange` cannot offer random access or +a `length` member. If you need random access, then just like any other range, +you can simply convert this range to an array via +$(LINK2 https://dlang.org/phobos/std_array.html#array, `std.array.array()`). + +A `ResultRange` becomes invalidated (and thus cannot be used) when the server +is sent another command on the same connection. When an invalidated `ResultRange` +is used, a `mysql.exceptions.MYXInvalidatedRange` is thrown. If you need to +send the server another command, but still access these results afterwords, +you can save the results for later by converting this range to an array via +$(LINK2 https://dlang.org/phobos/std_array.html#array, `std.array.array()`). + +Type_Mappings: $(TYPE_MAPPINGS) + +Example: +--- +ResultRange oneAtATime = myConnection.query("SELECT * from myTable"); +Row[] allAtOnce = myConnection.query("SELECT * from myTable").array; +--- ++/ +struct SafeResultRange +{ +private: +@safe: + Connection _con; + ResultSetHeaders _rsh; + SafeRow _row; // current row + string[] _colNames; + size_t[string] _colNameIndicies; + ulong _numRowsFetched; + ulong _commandID; // So we can keep track of when this is invalidated + + void ensureValid() const pure + { + enforce!MYXInvalidatedRange(isValid, + "This ResultRange has been invalidated and can no longer be used."); + } + +package(mysql): + this (Connection con, ResultSetHeaders rsh, string[] colNames) + { + _con = con; + _rsh = rsh; + _colNames = colNames; + _commandID = con.lastCommandID; + popFront(); + } + +public: + /++ + Check whether the range can still be used, or has been invalidated. + + A `ResultRange` becomes invalidated (and thus cannot be used) when the server + is sent another command on the same connection. When an invalidated `ResultRange` + is used, a `mysql.exceptions.MYXInvalidatedRange` is thrown. If you need to + send the server another command, but still access these results afterwords, + you can save the results for later by converting this range to an array via + $(LINK2 https://dlang.org/phobos/std_array.html#array, `std.array.array()`). + +/ + @property bool isValid() const pure nothrow + { + return _con !is null && _commandID == _con.lastCommandID; + } + + /// Check whether there are any rows left + @property bool empty() const pure nothrow + { + if(!isValid) + return true; + + return !_con._rowsPending; + } + + /++ + Gets the current row + +/ + @property inout(SafeRow) front() pure inout + { + ensureValid(); + enforce!MYX(!empty, "Attempted 'front' on exhausted result sequence."); + return _row; + } + + /++ + Progresses to the next row of the result set - that will then be 'front' + +/ + void popFront() + { + ensureValid(); + enforce!MYX(!empty, "Attempted 'popFront' when no more rows available"); + _row = _con.getNextRow(); + _numRowsFetched++; + } + + /++ + Get the current row as an associative array by column name + + Type_Mappings: $(TYPE_MAPPINGS) + +/ + MySQLVal[string] asAA() + { + ensureValid(); + enforce!MYX(!empty, "Attempted 'front' on exhausted result sequence."); + MySQLVal[string] aa; + foreach (size_t i, string s; _colNames) + aa[s] = _row._values[i]; + return aa; + } + + /// Get the names of all the columns + @property const(string)[] colNames() const pure nothrow { return _colNames; } + + /// An AA to lookup a column's index by name + @property const(size_t[string]) colNameIndicies() pure nothrow + { + if(_colNameIndicies is null) + { + foreach(index, name; _colNames) + _colNameIndicies[name] = index; + } + + return _colNameIndicies; + } + + /// Explicitly clean up the MySQL resources and cancel pending results + void close() + out{ assert(!isValid); } + body + { + if(isValid) + _con.purgeResult(); + } + + /++ + Get the number of rows retrieved so far. + + Note that this is not neccessarlly the same as the length of the range. + +/ + @property ulong rowCount() const pure nothrow { return _numRowsFetched; } +} + +/// ditto +struct UnsafeResultRange +{ + SafeResultRange safe; + alias safe this; + inout(UnsafeRow) front() inout { return inout(UnsafeRow)(safe.front); } + + Variant[string] asAA() + { + ensureValid(); + enforce!MYX(!safe.empty, "Attempted 'front' on exhausted result sequence."); + Variant[string] aa; + foreach (size_t i, string s; _colNames) + aa[s] = _row._values[i].asVariant; + return aa; + } +} + +/// ditto +UnsafeResultRange unsafe(SafeResultRange r) @safe +{ + return UnsafeResultRange(r); +} diff --git a/source/mysql/metadata.d b/source/mysql/metadata.d index 34d5cb86..ff0d3079 100644 --- a/source/mysql/metadata.d +++ b/source/mysql/metadata.d @@ -9,9 +9,11 @@ import std.exception; import mysql.safe.commands; import mysql.exceptions; import mysql.protocol.sockets; -import mysql.result; +import mysql.safe.result; import mysql.types; +@safe: + /// A struct to hold column metadata struct ColumnInfo { @@ -100,7 +102,6 @@ information that is available to the connected user. This may well be quite limi +/ struct MetaData { - @safe: import mysql.connection; private: diff --git a/source/mysql/package.d b/source/mysql/package.d index d5c977e2..8c3ea75d 100644 --- a/source/mysql/package.d +++ b/source/mysql/package.d @@ -1,4 +1,4 @@ -/++ +/++ Imports all of $(LINK2 https://github.com/mysql-d/mysql-native, mysql-native). MySQL_to_D_Type_Mappings: @@ -40,7 +40,7 @@ D_to_MySQL_Type_Mappings: $(TABLE $(TR $(TH D ) $(TH MySQL )) - + $(TR $(TD typeof(null) ) $(TD NULL )) $(TR $(TD bool ) $(TD BIT )) $(TR $(TD (u)byte ) $(TD (UNSIGNED) TINY )) @@ -49,7 +49,7 @@ $(TABLE $(TR $(TD (u)long ) $(TD (UNSIGNED) LONGLONG )) $(TR $(TD float ) $(TD (UNSIGNED) FLOAT )) $(TR $(TD double ) $(TD (UNSIGNED) DOUBLE )) - + $(TR $(TD $(STD_DATETIME_DATE Date) ) $(TD DATE )) $(TR $(TD $(STD_DATETIME_DATE TimeOfDay)) $(TD TIME )) $(TR $(TD $(STD_DATETIME_DATE Time) ) $(TD TIME )) @@ -62,19 +62,17 @@ $(TABLE $(TR $(TD other ) $(TD unsupported (throws) )) ) +Note: This by default imports the unsafe version of the MySQL API. Please +switch to the safe version (`import mysql.safe`) as this will be the default in +the future. If you would prefer to use the unsafe version, it is advised to use +the import `mysql.unsafe`, as this will be supported for at least one more +major version. +/ module mysql; -public import mysql.commands; -public import mysql.connection; -public import mysql.escape; -public import mysql.exceptions; -public import mysql.metadata; -public import mysql.pool; -public import mysql.prepared; -public import mysql.protocol.constants : SvrCapFlags; -public import mysql.result; -public import mysql.types; +// by default we do the unsafe API. This will change in a future version to the +// safe one. +public import mysql.unsafe; debug(MYSQLN_TESTS) version = DoCoreTests; debug(MYSQLN_CORE_TESTS) version = DoCoreTests; diff --git a/source/mysql/pool.d b/source/mysql/pool.d index a94761e9..c9b341d9 100644 --- a/source/mysql/pool.d +++ b/source/mysql/pool.d @@ -1,596 +1,3 @@ -/++ -Connect to a MySQL/MariaDB database using a connection pool. - -This provides various benefits over creating a new connection manually, -such as automatically reusing old connections, and automatic cleanup (no need to close -the connection when done). - -Internally, this is based on vibe.d's -$(LINK2 http://vibed.org/api/vibe.core.connectionpool/ConnectionPool, ConnectionPool). -You have to include vibe.d in your project to be able to use this class. -If you don't want to, refer to `mysql.connection.Connection`. -+/ module mysql.pool; -import std.conv; -import std.typecons; -import mysql.connection; -import mysql.prepared; -import mysql.protocol.constants; -debug(MYSQLN_TESTS) -{ - import mysql.test.common; -} - -version(Have_vibe_core) -{ - version = IncludeMySQLPool; - static if(is(typeof(ConnectionPool!Connection.init.removeUnused((c){})))) - version = HaveCleanupFunction; -} -version(MySQLDocs) -{ - version = IncludeMySQLPool; -} - -version(IncludeMySQLPool) -{ - version(Have_vibe_core) - import vibe.core.connectionpool; - else version(MySQLDocs) - { - /++ - Vibe.d's - $(LINK2 http://vibed.org/api/vibe.core.connectionpool/ConnectionPool, ConnectionPool) - class. - - Not actually included in module `mysql.pool`. Only listed here for - documentation purposes. For ConnectionPool and it's documentation, see: - $(LINK http://vibed.org/api/vibe.core.connectionpool/ConnectionPool) - +/ - class ConnectionPool(T) - { - /// See: $(LINK http://vibed.org/api/vibe.core.connectionpool/ConnectionPool.this) - this(Connection delegate() connection_factory, uint max_concurrent = (uint).max) - {} - - /// See: $(LINK http://vibed.org/api/vibe.core.connectionpool/ConnectionPool.lockConnection) - LockedConnection!T lockConnection() { return LockedConnection!T(); } - - /// See: $(LINK http://vibed.org/api/vibe.core.connectionpool/ConnectionPool.maxConcurrency) - uint maxConcurrency; - - /// See: $(LINK https://github.com/vibe-d/vibe-core/blob/24a83434e4c788ebb9859dfaecbe60ad0f6e9983/source/vibe/core/connectionpool.d#L113) - void removeUnused(scope void delegate(Connection conn) @safe nothrow disconnect_callback) - {} - } - - /++ - Vibe.d's - $(LINK2 http://vibed.org/api/vibe.core.connectionpool/LockedConnection, LockedConnection) - struct. - - Not actually included in module `mysql.pool`. Only listed here for - documentation purposes. For LockedConnection and it's documentation, see: - $(LINK http://vibed.org/api/vibe.core.connectionpool/LockedConnection) - +/ - struct LockedConnection(Connection) { Connection c; alias c this; } - } - - /++ - Connect to a MySQL/MariaDB database using a connection pool. - - This provides various benefits over creating a new connection manually, - such as automatically reusing old connections, and automatic cleanup (no need to close - the connection when done). - - Internally, this is based on vibe.d's - $(LINK2 http://vibed.org/api/vibe.core.connectionpool/ConnectionPool, ConnectionPool). - You have to include vibe.d in your project to be able to use this class. - If you don't want to, refer to `mysql.connection.Connection`. - +/ - class MySQLPool - { - private - { - string m_host; - string m_user; - string m_password; - string m_database; - ushort m_port; - SvrCapFlags m_capFlags; - alias NewConnectionDelegate = void delegate(Connection) @safe; - NewConnectionDelegate m_onNewConnection; - ConnectionPool!Connection m_pool; - PreparedRegistrations!PreparedInfo preparedRegistrations; - - struct PreparedInfo - { - bool queuedForRelease = false; - } - - } - @safe: - - /// Sets up a connection pool with the provided connection settings. - /// - /// The optional `onNewConnection` param allows you to set a callback - /// which will be run every time a new connection is created. - this(string host, string user, string password, string database, - ushort port = 3306, uint maxConcurrent = (uint).max, - SvrCapFlags capFlags = defaultClientFlags, - NewConnectionDelegate onNewConnection = null) - { - m_host = host; - m_user = user; - m_password = password; - m_database = database; - m_port = port; - m_capFlags = capFlags; - m_onNewConnection = onNewConnection; - m_pool = new ConnectionPool!Connection(&createConnection); - } - - ///ditto - this(string host, string user, string password, string database, - ushort port, SvrCapFlags capFlags, NewConnectionDelegate onNewConnection = null) - { - this(host, user, password, database, port, (uint).max, capFlags, onNewConnection); - } - - ///ditto - this(string host, string user, string password, string database, - ushort port, NewConnectionDelegate onNewConnection) - { - this(host, user, password, database, port, (uint).max, defaultClientFlags, onNewConnection); - } - - ///ditto - this(string connStr, uint maxConcurrent = (uint).max, SvrCapFlags capFlags = defaultClientFlags, - NewConnectionDelegate onNewConnection = null) - { - auto parts = Connection.parseConnectionString(connStr); - this(parts[0], parts[1], parts[2], parts[3], to!ushort(parts[4]), capFlags, onNewConnection); - } - - ///ditto - this(string connStr, SvrCapFlags capFlags, NewConnectionDelegate onNewConnection = null) - { - this(connStr, (uint).max, capFlags, onNewConnection); - } - - ///ditto - this(string connStr, NewConnectionDelegate onNewConnection) - { - this(connStr, (uint).max, defaultClientFlags, onNewConnection); - } - - /++ - Obtain a connection. If one isn't available, a new one will be created. - - The connection returned is actually a `LockedConnection!Connection`, - but it uses `alias this`, and so can be used just like a Connection. - (See vibe.d's - $(LINK2 http://vibed.org/api/vibe.core.connectionpool/LockedConnection, LockedConnection documentation).) - - No other fiber will be given this `mysql.connection.Connection` as long as your fiber still holds it. - - There is no need to close, release or unlock this connection. It is - reference-counted and will automatically be returned to the pool once - your fiber is done with it. - - If you have passed any prepared statements to `autoRegister` - or `autoRelease`, then those statements will automatically be - registered/released on the connection. (Currently, this automatic - register/release may actually occur upon the first command sent via - the connection.) - +/ - LockedConnection!Connection lockConnection() - { - auto conn = m_pool.lockConnection(); - if(conn.closed) - conn.reconnect(); - - applyAuto(conn); - return conn; - } - - /// Applies any `autoRegister`/`autoRelease` settings to a connection, - /// if necessary. - private void applyAuto(T)(T conn) - { - foreach(sql, info; preparedRegistrations.directLookup) - { - auto registeredOnPool = !info.queuedForRelease; - auto registeredOnConnection = conn.isRegistered(sql); - - if(registeredOnPool && !registeredOnConnection) // Need to register? - conn.register(sql); - else if(!registeredOnPool && registeredOnConnection) // Need to release? - conn.release(sql); - } - } - - private Connection createConnection() @safe - { - auto conn = new Connection(m_host, m_user, m_password, m_database, m_port, m_capFlags); - - if(m_onNewConnection) - m_onNewConnection(conn); - - return conn; - } - - /// Get/set a callback delegate to be run every time a new connection - /// is created. - @property void onNewConnection(NewConnectionDelegate onNewConnection) - { - m_onNewConnection = onNewConnection; - } - - ///ditto - @property void delegate(Connection) @safe onNewConnection() - { - return m_onNewConnection; - } - - @("onNewConnection") - debug(MYSQLN_TESTS) - unittest - { - auto count = 0; - void callback(Connection conn) - { - count++; - } - - // Test getting/setting - auto poolA = new MySQLPool(testConnectionStr, &callback); - auto poolB = new MySQLPool(testConnectionStr); - auto poolNoCallback = new MySQLPool(testConnectionStr); - - assert(poolA.onNewConnection == &callback); - assert(poolB.onNewConnection is null); - assert(poolNoCallback.onNewConnection is null); - - poolB.onNewConnection = &callback; - assert(poolB.onNewConnection == &callback); - assert(count == 0); - - // Ensure callback is called - { - auto connA = poolA.lockConnection(); - assert(!connA.closed); - assert(count == 1); - - auto connB = poolB.lockConnection(); - assert(!connB.closed); - assert(count == 2); - } - - // Ensure works with no callback - { - auto oldCount = count; - auto poolC = new MySQLPool(testConnectionStr); - auto connC = poolC.lockConnection(); - assert(!connC.closed); - assert(count == oldCount); - } - } - - /++ - Forwards to vibe.d's - $(LINK2 http://vibed.org/api/vibe.core.connectionpool/ConnectionPool.maxConcurrency, ConnectionPool.maxConcurrency) - +/ - @property uint maxConcurrency() - { - return m_pool.maxConcurrency; - } - - ///ditto - @property void maxConcurrency(uint maxConcurrent) - { - m_pool.maxConcurrency = maxConcurrent; - } - - /++ - Set a prepared statement to be automatically registered on all - connections received from this pool. - - This also clears any `autoRelease` which may have been set for this statement. - - Calling this is not strictly necessary, as a prepared statement will - automatically be registered upon its first use on any `Connection`. - This is provided for those who prefer eager registration over lazy - for performance reasons. - - Once this has been called, obtaining a connection via `lockConnection` - will automatically register the prepared statement on the connection - if it isn't already registered on the connection. This single - registration safely persists after the connection is reclaimed by the - pool and locked again by another Vibe.d task. - - Note, due to the way Vibe.d works, it is not possible to eagerly - register or release a statement on all connections already sitting - in the pool. This can only be done when locking a connection. - - You can stop the pool from continuing to auto-register the statement - by calling either `autoRelease` or `clearAuto`. - +/ - void autoRegister(Prepared prepared) - { - autoRegister(prepared.sql); - } - - ///ditto - void autoRegister(const(char[]) sql) - { - preparedRegistrations.registerIfNeeded(sql, (sql) => PreparedInfo()); - } - - /++ - Set a prepared statement to be automatically released from all - connections received from this pool. - - This also clears any `autoRegister` which may have been set for this statement. - - Calling this is not strictly necessary. The server considers prepared - statements to be per-connection, so they'll go away when the connection - closes anyway. This is provided in case direct control is actually needed. - - Once this has been called, obtaining a connection via `lockConnection` - will automatically release the prepared statement from the connection - if it isn't already releases from the connection. - - Note, due to the way Vibe.d works, it is not possible to eagerly - register or release a statement on all connections already sitting - in the pool. This can only be done when locking a connection. - - You can stop the pool from continuing to auto-release the statement - by calling either `autoRegister` or `clearAuto`. - +/ - void autoRelease(Prepared prepared) - { - autoRelease(prepared.sql); - } - - ///ditto - void autoRelease(const(char[]) sql) - { - preparedRegistrations.queueForRelease(sql); - } - - /// Is the given statement set to be automatically registered on all - /// connections obtained from this connection pool? - bool isAutoRegistered(Prepared prepared) - { - return isAutoRegistered(prepared.sql); - } - ///ditto - bool isAutoRegistered(const(char[]) sql) - { - return isAutoRegistered(preparedRegistrations[sql]); - } - ///ditto - package bool isAutoRegistered(Nullable!PreparedInfo info) - { - return info.isNull || !info.get.queuedForRelease; - } - - /// Is the given statement set to be automatically released on all - /// connections obtained from this connection pool? - bool isAutoReleased(Prepared prepared) - { - return isAutoReleased(prepared.sql); - } - ///ditto - bool isAutoReleased(const(char[]) sql) - { - return isAutoReleased(preparedRegistrations[sql]); - } - ///ditto - package bool isAutoReleased(Nullable!PreparedInfo info) - { - return info.isNull || info.get.queuedForRelease; - } - - /++ - Is the given statement set for NEITHER auto-register - NOR auto-release on connections obtained from - this connection pool? - - Equivalent to `!isAutoRegistered && !isAutoReleased`. - +/ - bool isAutoCleared(Prepared prepared) - { - return isAutoCleared(prepared.sql); - } - ///ditto - bool isAutoCleared(const(char[]) sql) - { - return isAutoCleared(preparedRegistrations[sql]); - } - ///ditto - package bool isAutoCleared(Nullable!PreparedInfo info) - { - return info.isNull; - } - - /++ - Removes any `autoRegister` or `autoRelease` which may have been set - for this prepared statement. - - Does nothing if the statement has not been set for auto-register or auto-release. - - This releases any relevent memory for potential garbage collection. - +/ - void clearAuto(Prepared prepared) - { - return clearAuto(prepared.sql); - } - ///ditto - void clearAuto(const(char[]) sql) - { - preparedRegistrations.directLookup.remove(sql); - } - - /++ - Removes ALL prepared statement `autoRegister` and `autoRelease` which have been set. - - This releases all relevent memory for potential garbage collection. - +/ - void clearAllRegistrations() - { - preparedRegistrations.clear(); - } - - version(MySQLDocs) - { - /++ - Removes all unused connections from the pool. This can - be used to clean up before exiting the program to - ensure the event core driver can be properly shut down. - - Note: this is only available if vibe-core 1.7.0 or later is being - used. - +/ - void removeUnusedConnections() @safe {} - } - else version(HaveCleanupFunction) - { - void removeUnusedConnections() @safe - { - // Note: we squelch all exceptions here, because vibe-core - // requires the function be nothrow, and because an exception - // thrown while closing is probably not important enough to - // interrupt cleanup. - m_pool.removeUnused((conn) @trusted nothrow { - try { - conn.close(); - } catch(Exception) {} - }); - } - } - } - - @("registration") - debug(MYSQLN_TESTS) - unittest - { - import mysql.commands; - auto pool = new MySQLPool(testConnectionStr); - - // Setup - Connection cn = pool.lockConnection(); - cn.exec("DROP TABLE IF EXISTS `poolRegistration`"); - cn.exec("CREATE TABLE `poolRegistration` ( - `data` LONGBLOB - ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - immutable sql = "SELECT * from `poolRegistration`"; - //auto cn2 = pool.lockConnection(); // Seems to return the same connection as `cn` - auto cn2 = pool.createConnection(); - pool.applyAuto(cn2); - assert(cn !is cn2); - - // Tests: - // Initial - assert(pool.isAutoCleared(sql)); - assert(pool.isAutoRegistered(sql)); - assert(pool.isAutoReleased(sql)); - assert(!cn.isRegistered(sql)); - assert(!cn2.isRegistered(sql)); - - // Register on connection #1 - auto prepared = cn.prepare(sql); - { - assert(pool.isAutoCleared(sql)); - assert(pool.isAutoRegistered(sql)); - assert(pool.isAutoReleased(sql)); - assert(cn.isRegistered(sql)); - assert(!cn2.isRegistered(sql)); - - //auto cn3 = pool.lockConnection(); // Seems to return the same connection as `cn` - auto cn3 = pool.createConnection(); - pool.applyAuto(cn3); - assert(!cn3.isRegistered(sql)); - } - - // autoRegister - pool.autoRegister(prepared); - { - assert(!pool.isAutoCleared(sql)); - assert(pool.isAutoRegistered(sql)); - assert(!pool.isAutoReleased(sql)); - assert(cn.isRegistered(sql)); - assert(!cn2.isRegistered(sql)); - - //auto cn3 = pool.lockConnection(); // Seems to return the *same* connection as `cn` - auto cn3 = pool.createConnection(); - pool.applyAuto(cn3); - assert(cn3.isRegistered(sql)); - } - - // autoRelease - pool.autoRelease(prepared); - { - assert(!pool.isAutoCleared(sql)); - assert(!pool.isAutoRegistered(sql)); - assert(pool.isAutoReleased(sql)); - assert(cn.isRegistered(sql)); - assert(!cn2.isRegistered(sql)); - - //auto cn3 = pool.lockConnection(); // Seems to return the same connection as `cn` - auto cn3 = pool.createConnection(); - pool.applyAuto(cn3); - assert(!cn3.isRegistered(sql)); - } - - // clearAuto - pool.clearAuto(prepared); - { - assert(pool.isAutoCleared(sql)); - assert(pool.isAutoRegistered(sql)); - assert(pool.isAutoReleased(sql)); - assert(cn.isRegistered(sql)); - assert(!cn2.isRegistered(sql)); - - //auto cn3 = pool.lockConnection(); // Seems to return the same connection as `cn` - auto cn3 = pool.createConnection(); - pool.applyAuto(cn3); - assert(!cn3.isRegistered(sql)); - } - } - - @("closedConnection") // "cct" - debug(MYSQLN_TESTS) - { - MySQLPool cctPool; - int cctCount=0; - - void cctStart() - { - import std.array; - import mysql.commands; - - cctPool = new MySQLPool(testConnectionStr); - cctPool.onNewConnection = (Connection conn) { cctCount++; }; - assert(cctCount == 0); - - auto cn = cctPool.lockConnection(); - assert(!cn.closed); - cn.close(); - assert(cn.closed); - assert(cctCount == 1); - } - - unittest - { - cctStart(); - assert(cctCount == 1); - - auto cn = cctPool.lockConnection(); - assert(cctCount == 1); - assert(!cn.closed); - } - } -} +public import mysql.unsafe.pool; diff --git a/source/mysql/prepared.d b/source/mysql/prepared.d index efb0d52c..920d016d 100644 --- a/source/mysql/prepared.d +++ b/source/mysql/prepared.d @@ -1,762 +1,3 @@ -/// Use a DB via SQL prepared statements. module mysql.prepared; -import std.exception; -import std.range; -import std.traits; -import std.typecons; -import std.variant; - -import mysql.safe.commands; -import mysql.exceptions; -import mysql.protocol.comms; -import mysql.protocol.constants; -import mysql.protocol.packets; -import mysql.result; -import mysql.types; -debug(MYSQLN_TESTS) - import mysql.test.common; - -/++ -A struct to represent specializations of prepared statement parameters. - -If you need to send large objects to the database it might be convenient to -send them in pieces. The `chunkSize` and `chunkDelegate` variables allow for this. -If both are provided then the corresponding column will be populated by calling the delegate repeatedly. -The source should fill the indicated slice with data and arrange for the delegate to -return the length of the data supplied (in bytes). If that is less than the `chunkSize` -then the chunk will be assumed to be the last one. -+/ -struct ParameterSpecialization -{ - import mysql.protocol.constants; - - size_t pIndex; //parameter number 0 - number of params-1 - SQLType type = SQLType.INFER_FROM_D_TYPE; - uint chunkSize; /// In bytes - uint delegate(ubyte[]) @safe chunkDelegate; -} -///ditto -alias PSN = ParameterSpecialization; - -@("paramSpecial") -debug(MYSQLN_TESTS) -unittest -{ - import std.array; - import std.range; - import mysql.connection; - import mysql.test.common; - mixin(scopedCn); - - // Setup - cn.exec("DROP TABLE IF EXISTS `paramSpecial`"); - cn.exec("CREATE TABLE `paramSpecial` ( - `data` LONGBLOB - ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - - immutable totalSize = 1000; // Deliberately not a multiple of chunkSize below - auto alph = cast(const(ubyte)[]) "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - auto data = alph.cycle.take(totalSize).array; - - int chunkSize; - const(ubyte)[] dataToSend; - bool finished; - uint sender(ubyte[] chunk) - { - assert(!finished); - assert(chunk.length == chunkSize); - - if(dataToSend.length < chunkSize) - { - auto actualSize = cast(uint) dataToSend.length; - chunk[0..actualSize] = dataToSend[]; - finished = true; - dataToSend.length = 0; - return actualSize; - } - else - { - chunk[] = dataToSend[0..chunkSize]; - dataToSend = dataToSend[chunkSize..$]; - return chunkSize; - } - } - - immutable selectSQL = "SELECT `data` FROM `paramSpecial`"; - - // Sanity check - cn.exec("INSERT INTO `paramSpecial` VALUES (\""~(cast(string)data)~"\")"); - auto value = cn.queryValue(selectSQL); - assert(!value.isNull); - assert(value.get == data); - - { - // Clear table - cn.exec("DELETE FROM `paramSpecial`"); - value = cn.queryValue(selectSQL); // Ensure deleted - assert(value.isNull); - - // Test: totalSize as a multiple of chunkSize - chunkSize = 100; - assert(cast(int)(totalSize / chunkSize) * chunkSize == totalSize); - auto paramSpecial = ParameterSpecialization(0, SQLType.INFER_FROM_D_TYPE, chunkSize, &sender); - - finished = false; - dataToSend = data; - auto prepared = cn.prepare("INSERT INTO `paramSpecial` VALUES (?)"); - prepared.setArg(0, cast(ubyte[])[], paramSpecial); - assert(cn.exec(prepared) == 1); - value = cn.queryValue(selectSQL); - assert(!value.isNull); - assert(value.get == data); - } - - { - // Clear table - cn.exec("DELETE FROM `paramSpecial`"); - value = cn.queryValue(selectSQL); // Ensure deleted - assert(value.isNull); - - // Test: totalSize as a non-multiple of chunkSize - chunkSize = 64; - assert(cast(int)(totalSize / chunkSize) * chunkSize != totalSize); - auto paramSpecial = ParameterSpecialization(0, SQLType.INFER_FROM_D_TYPE, chunkSize, &sender); - - finished = false; - dataToSend = data; - auto prepared = cn.prepare("INSERT INTO `paramSpecial` VALUES (?)"); - prepared.setArg(0, cast(ubyte[])[], paramSpecial); - assert(cn.exec(prepared) == 1); - value = cn.queryValue(selectSQL); - assert(!value.isNull); - assert(value.get == data); - } -} - -/++ -Encapsulation of a prepared statement. - -Create this via the function `mysql.connection.prepare`. Set your arguments (if any) via -the functions provided, and then run the statement by passing it to -`mysql.commands.exec`/`mysql.commands.query`/etc in place of the sql string parameter. - -Commands that are expected to return a result set - queries - have distinctive -methods that are enforced. That is it will be an error to call such a method -with an SQL command that does not produce a result set. So for commands like -SELECT, use the `mysql.commands.query` functions. For other commands, like -INSERT/UPDATE/CREATE/etc, use `mysql.commands.exec`. -+/ -struct Prepared -{ - @safe: -private: - const(char)[] _sql; - -package: - ushort _numParams; /// Number of parameters this prepared statement takes - PreparedStmtHeaders _headers; - MySQLVal[] _inParams; - ParameterSpecialization[] _psa; - ColumnSpecialization[] _columnSpecials; - ulong _lastInsertID; - - ExecQueryImplInfo getExecQueryImplInfo(uint statementId) - { - return ExecQueryImplInfo(true, null, statementId, _headers, _inParams, _psa); - } - -public: - /++ - Constructor. You probably want `mysql.connection.prepare` instead of this. - - Call `mysqln.connection.prepare` instead of this, unless you are creating - your own transport bypassing `mysql.connection.Connection` entirely. - The prepared statement must be registered on the server BEFORE this is - called (which `mysqln.connection.prepare` does). - - Internally, the result of a successful outcome will be a statement handle - an ID - - for the prepared statement, a count of the parameters required for - execution of the statement, and a count of the columns that will be present - in any result set that the command generates. - - The server will then proceed to send prepared statement headers, - including parameter descriptions, and result set field descriptions, - followed by an EOF packet. - +/ - this(const(char[]) sql, PreparedStmtHeaders headers, ushort numParams) - { - this._sql = sql; - this._headers = headers; - this._numParams = numParams; - _inParams.length = numParams; - _psa.length = numParams; - } - - /++ - Prepared statement parameter setter. - - The value may, but doesn't have to be, wrapped in a MySQLVal. If so, - null is handled correctly. - - The value may, but doesn't have to be, a pointer to the desired value. - - The value may, but doesn't have to be, wrapped in a Nullable!T. If so, - null is handled correctly. - - The value can be null. - - Parameter specializations (ie, for chunked transfer) can be added if required. - If you wish to use chunked transfer (via `psn`), note that you must supply - a dummy value for `val` that's typed `ubyte[]`. For example: `cast(ubyte[])[]`. - - Type_Mappings: $(TYPE_MAPPINGS) - - Params: index = The zero based index - +/ - void setArg(T)(size_t index, T val, ParameterSpecialization psn = PSN(0, SQLType.INFER_FROM_D_TYPE, 0, null)) - if(!isInstanceOf!(Nullable, T) && !is(T == Variant)) - { - // Now in theory we should be able to check the parameter type here, since the - // protocol is supposed to send us type information for the parameters, but this - // capability seems to be broken. This assertion is supported by the fact that - // the same information is not available via the MySQL C API either. It is up - // to the programmer to ensure that appropriate type information is embodied - // in the variant array, or provided explicitly. This sucks, but short of - // having a client side SQL parser I don't see what can be done. - - enforce!MYX(index < _numParams, "Parameter index out of range."); - - _inParams[index] = val; - psn.pIndex = index; - _psa[index] = psn; - } - - ///ditto - void setArg(T)(size_t index, Nullable!T val, ParameterSpecialization psn = PSN(0, SQLType.INFER_FROM_D_TYPE, 0, null)) - { - if(val.isNull) - setArg(index, null, psn); - else - setArg(index, val.get(), psn); - } - - void setArg(T)(size_t index, T val, ParameterSpecialization psn = PSN(0, SQLType.INFER_FROM_D_TYPE, 0, null)) @system - if(is(T == Variant)) - { - enforce!MYX(index < _numParams, "Parameter index out of range."); - - _inParams[index] = _toVal(val); - psn.pIndex = index; - _psa[index] = psn; - } - - @("setArg-typeMods") - debug(MYSQLN_TESTS) - unittest - { - import mysql.test.common; - mixin(scopedCn); - - // Setup - cn.exec("DROP TABLE IF EXISTS `setArg-typeMods`"); - cn.exec("CREATE TABLE `setArg-typeMods` ( - `i` INTEGER - ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - - auto insertSQL = "INSERT INTO `setArg-typeMods` VALUES (?)"; - - // Sanity check - { - int i = 111; - assert(cn.exec(insertSQL, i) == 1); - auto value = cn.queryValue("SELECT `i` FROM `setArg-typeMods`"); - assert(!value.isNull); - assert(value.get == i); - } - - // Test const(int) - { - const(int) i = 112; - assert(cn.exec(insertSQL, i) == 1); - } - - // Test immutable(int) - { - immutable(int) i = 113; - assert(cn.exec(insertSQL, i) == 1); - } - - // Note: Variant doesn't seem to support - // `shared(T)` or `shared(const(T)`. Only `shared(immutable(T))`. - - // Further note, shared immutable(int) is really - // immutable(int). This test is a duplicate, so removed. - // Test shared immutable(int) - /*{ - shared immutable(int) i = 113; - assert(cn.exec(insertSQL, i) == 1); - }*/ - } - - /++ - Bind a tuple of D variables to the parameters of a prepared statement. - - You can use this method to bind a set of variables if you don't need any specialization, - that is chunked transfer is not neccessary. - - The tuple must match the required number of parameters, and it is the programmer's - responsibility to ensure that they are of appropriate types. - - Type_Mappings: $(TYPE_MAPPINGS) - +/ - void setArgs(T...)(T args) - if(T.length == 0 || (!is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]))) - { - enforce!MYX(args.length == _numParams, "Argument list supplied does not match the number of parameters."); - - foreach (size_t i, arg; args) - setArg(i, arg); - } - - /++ - Bind a MySQLVal[] as the parameters of a prepared statement. - - You can use this method to bind a set of variables in MySQLVal form to - the parameters of a prepared statement. - - Parameter specializations (ie, for chunked transfer) can be added if required. - If you wish to use chunked transfer (via `psn`), note that you must supply - a dummy value for `val` that's typed `ubyte[]`. For example: `cast(ubyte[])[]`. - - This method could be - used to add records from a data entry form along the lines of - ------------ - auto stmt = conn.prepare("INSERT INTO `table42` VALUES(?, ?, ?)"); - DataRecord dr; // Some data input facility - ulong ra; - do - { - dr.get(); - stmt.setArgs(dr("Name"), dr("City"), dr("Whatever")); - ulong rowsAffected = conn.exec(stmt); - } while(!dr.done); - ------------ - - Type_Mappings: $(TYPE_MAPPINGS) - - Params: - args = External list of MySQLVal to be used as parameters - psnList = Any required specializations - +/ - void setArgs(MySQLVal[] args, ParameterSpecialization[] psnList=null) - { - enforce!MYX(args.length == _numParams, "Param count supplied does not match prepared statement"); - _inParams[] = args[]; - if (psnList !is null) - { - foreach (PSN psn; psnList) - _psa[psn.pIndex] = psn; - } - } - - /// ditto - void setArgs(Variant[] args, ParameterSpecialization[] psnList=null) @system - { - enforce!MYX(args.length == _numParams, "Param count supplied does not match prepared statement"); - foreach(i, ref arg; args) - setArg(i, arg); - if (psnList !is null) - { - foreach (PSN psn; psnList) - _psa[psn.pIndex] = psn; - } - } - - /++ - Prepared statement parameter getter. - - Type_Mappings: $(TYPE_MAPPINGS) - - Params: index = The zero based index - - Note: The type of getArg's return is now MySQLVal. As a stop-gap measure, - mysql-native provides the vGetArg version. This version will be removed - in a future update. - +/ - MySQLVal getArg(size_t index) - { - enforce!MYX(index < _numParams, "Parameter index out of range."); - return _inParams[index]; - } - - /// ditto - Variant vGetArg(size_t index) @system - { - // convert to Variant. - return getArg(index).asVariant; - } - - /++ - Sets a prepared statement parameter to NULL. - - This is here mainly for legacy reasons. You can set a field to null - simply by saying `prepared.setArg(index, null);` - - Type_Mappings: $(TYPE_MAPPINGS) - - Params: index = The zero based index - +/ - void setNullArg(size_t index) - { - setArg(index, null); - } - - /// Gets the SQL command for this prepared statement. - const(char)[] sql() pure const - { - return _sql; - } - - @("setNullArg") - debug(MYSQLN_TESTS) - unittest - { - import mysql.connection; - import mysql.test.common; - mixin(scopedCn); - - cn.exec("DROP TABLE IF EXISTS `setNullArg`"); - cn.exec("CREATE TABLE `setNullArg` ( - `val` INTEGER - ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - - immutable insertSQL = "INSERT INTO `setNullArg` VALUES (?)"; - immutable selectSQL = "SELECT * FROM `setNullArg`"; - auto preparedInsert = cn.prepare(insertSQL); - assert(preparedInsert.sql == insertSQL); - SafeRow[] rs; - - { - Nullable!int nullableInt; - nullableInt.nullify(); - preparedInsert.setArg(0, nullableInt); - assert(preparedInsert.getArg(0).kind == MySQLVal.Kind.Null); - nullableInt = 7; - preparedInsert.setArg(0, nullableInt); - assert(preparedInsert.getArg(0) == 7); - - nullableInt.nullify(); - preparedInsert.setArgs(nullableInt); - assert(preparedInsert.getArg(0).kind == MySQLVal.Kind.Null); - nullableInt = 7; - preparedInsert.setArgs(nullableInt); - assert(preparedInsert.getArg(0) == 7); - } - - preparedInsert.setArg(0, 5); - cn.exec(preparedInsert); - rs = cn.query(selectSQL).array; - assert(rs.length == 1); - assert(rs[0][0] == 5); - - preparedInsert.setArg(0, null); - cn.exec(preparedInsert); - rs = cn.query(selectSQL).array; - assert(rs.length == 2); - assert(rs[0][0] == 5); - assert(rs[1].isNull(0)); - assert(rs[1][0].kind == MySQLVal.Kind.Null); - - preparedInsert.setArg(0, MySQLVal(null)); - cn.exec(preparedInsert); - rs = cn.query(selectSQL).array; - assert(rs.length == 3); - assert(rs[0][0] == 5); - assert(rs[1].isNull(0)); - assert(rs[2].isNull(0)); - assert(rs[1][0].kind == MySQLVal.Kind.Null); - assert(rs[2][0].kind == MySQLVal.Kind.Null); - } - - /// Gets the number of arguments this prepared statement expects to be passed in. - @property ushort numArgs() pure const nothrow - { - return _numParams; - } - - /// After a command that inserted a row into a table with an auto-increment - /// ID column, this method allows you to retrieve the last insert ID generated - /// from this prepared statement. - @property ulong lastInsertID() pure const nothrow { return _lastInsertID; } - - @("lastInsertID") - debug(MYSQLN_TESTS) - unittest - { - import mysql.connection; - mixin(scopedCn); - cn.exec("DROP TABLE IF EXISTS `testPreparedLastInsertID`"); - cn.exec("CREATE TABLE `testPreparedLastInsertID` ( - `a` INTEGER NOT NULL AUTO_INCREMENT, - PRIMARY KEY (a) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - - auto stmt = cn.prepare("INSERT INTO `testPreparedLastInsertID` VALUES()"); - cn.exec(stmt); - assert(stmt.lastInsertID == 1); - cn.exec(stmt); - assert(stmt.lastInsertID == 2); - cn.exec(stmt); - assert(stmt.lastInsertID == 3); - } - - /// Gets the prepared header's field descriptions. - @property FieldDescription[] preparedFieldDescriptions() pure { return _headers.fieldDescriptions; } - - /// Gets the prepared header's param descriptions. - @property ParamDescription[] preparedParamDescriptions() pure { return _headers.paramDescriptions; } - - /// Get/set the column specializations. - @property ColumnSpecialization[] columnSpecials() pure { return _columnSpecials; } - - ///ditto - @property void columnSpecials(ColumnSpecialization[] csa) pure { _columnSpecials = csa; } -} - -/// Template constraint for `PreparedRegistrations` -private enum isPreparedRegistrationsPayload(Payload) = - __traits(compiles, (){ - static assert(Payload.init.queuedForRelease == false); - Payload p; - p.queuedForRelease = true; - }); - -/++ -Common functionality for recordkeeping of prepared statement registration -and queueing for unregister. - -Used by `Connection` and `MySQLPool`. - -Templated on payload type. The payload should be an aggregate that includes -the field: `bool queuedForRelease = false;` - -Allowing access to `directLookup` from other parts of mysql-native IS intentional. -`PreparedRegistrations` isn't intended as 100% encapsulation, it's mainly just -to factor out common functionality needed by both `Connection` and `MySQLPool`. -+/ -package struct PreparedRegistrations(Payload) - if( isPreparedRegistrationsPayload!Payload) -{ - @safe: - /++ - Lookup payload by sql string. - - Allowing access to `directLookup` from other parts of mysql-native IS intentional. - `PreparedRegistrations` isn't intended as 100% encapsulation, it's mainly just - to factor out common functionality needed by both `Connection` and `MySQLPool`. - +/ - Payload[const(char[])] directLookup; - - /// Returns null if not found - Nullable!Payload opIndex(const(char[]) sql) pure nothrow - { - Nullable!Payload result; - - auto pInfo = sql in directLookup; - if(pInfo) - result = *pInfo; - - return result; - } - - /// Set `queuedForRelease` flag for a statement in `directLookup`. - /// Does nothing if statement not in `directLookup`. - private void setQueuedForRelease(const(char[]) sql, bool value) - { - if(auto pInfo = sql in directLookup) - { - pInfo.queuedForRelease = value; - directLookup[sql] = *pInfo; - } - } - - /// Queue a prepared statement for release. - void queueForRelease(const(char[]) sql) - { - setQueuedForRelease(sql, true); - } - - /// Remove a statement from the queue to be released. - void unqueueForRelease(const(char[]) sql) - { - setQueuedForRelease(sql, false); - } - - /// Queues all prepared statements for release. - void queueAllForRelease() - { - foreach(sql, info; directLookup) - queueForRelease(sql); - } - - // Note: AA.clear does not invalidate any keys or values. In fact, it - // should really be safe/trusted, but is not. Therefore, we mark this - // as trusted. - /// Eliminate all records of both registered AND queued-for-release statements. - void clear() @trusted - { - static if(__traits(compiles, (){ int[int] aa; aa.clear(); })) - directLookup.clear(); - else - directLookup = null; - } - - /// If already registered, simply returns the cached Payload. - Payload registerIfNeeded(const(char[]) sql, Payload delegate(const(char[])) @safe doRegister) - out(info) - { - // I'm confident this can't currently happen, but - // let's make sure that doesn't change. - assert(!info.queuedForRelease); - } - body - { - if(auto pInfo = sql in directLookup) - { - // The statement is registered. It may, or may not, be queued - // for release. Either way, all we need to do is make sure it's - // un-queued and then return. - pInfo.queuedForRelease = false; - return *pInfo; - } - - auto info = doRegister(sql); - directLookup[sql] = info; - - return info; - } -} - -// Test PreparedRegistrations -debug(MYSQLN_TESTS) -{ - // Test template constraint - struct TestPreparedRegistrationsBad1 { } - struct TestPreparedRegistrationsBad2 { bool foo = false; } - struct TestPreparedRegistrationsBad3 { int queuedForRelease = 1; } - struct TestPreparedRegistrationsBad4 { bool queuedForRelease = true; } - struct TestPreparedRegistrationsGood1 { bool queuedForRelease = false; } - struct TestPreparedRegistrationsGood2 { bool queuedForRelease = false; const(char)[] id; } - - static assert(!isPreparedRegistrationsPayload!int); - static assert(!isPreparedRegistrationsPayload!bool); - static assert(!isPreparedRegistrationsPayload!TestPreparedRegistrationsBad1); - static assert(!isPreparedRegistrationsPayload!TestPreparedRegistrationsBad2); - static assert(!isPreparedRegistrationsPayload!TestPreparedRegistrationsBad3); - static assert(!isPreparedRegistrationsPayload!TestPreparedRegistrationsBad4); - //static assert(isPreparedRegistrationsPayload!TestPreparedRegistrationsGood1); - //static assert(isPreparedRegistrationsPayload!TestPreparedRegistrationsGood2); - PreparedRegistrations!TestPreparedRegistrationsGood1 testPreparedRegistrationsGood1; - PreparedRegistrations!TestPreparedRegistrationsGood2 testPreparedRegistrationsGood2; - - @("PreparedRegistrations") - unittest - { - // Test init - PreparedRegistrations!TestPreparedRegistrationsGood2 pr; - assert(pr.directLookup.keys.length == 0); - - void resetData(bool isQueued1, bool isQueued2, bool isQueued3) - { - pr.directLookup["1"] = TestPreparedRegistrationsGood2(isQueued1, "1"); - pr.directLookup["2"] = TestPreparedRegistrationsGood2(isQueued2, "2"); - pr.directLookup["3"] = TestPreparedRegistrationsGood2(isQueued3, "3"); - assert(pr.directLookup.keys.length == 3); - } - - // Test resetData (sanity check) - resetData(false, true, false); - assert(pr.directLookup["1"] == TestPreparedRegistrationsGood2(false, "1")); - assert(pr.directLookup["2"] == TestPreparedRegistrationsGood2(true, "2")); - assert(pr.directLookup["3"] == TestPreparedRegistrationsGood2(false, "3")); - - // Test opIndex - resetData(false, true, false); - pr.directLookup["1"] = TestPreparedRegistrationsGood2(false, "1"); - pr.directLookup["2"] = TestPreparedRegistrationsGood2(true, "2"); - pr.directLookup["3"] = TestPreparedRegistrationsGood2(false, "3"); - assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); - assert(pr["2"] == TestPreparedRegistrationsGood2(true, "2")); - assert(pr["3"] == TestPreparedRegistrationsGood2(false, "3")); - assert(pr["4"].isNull); - - // Test queueForRelease - resetData(false, true, false); - pr.queueForRelease("2"); - assert(pr.directLookup.keys.length == 3); - assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); - assert(pr["2"] == TestPreparedRegistrationsGood2(true, "2")); - assert(pr["3"] == TestPreparedRegistrationsGood2(false, "3")); - - pr.queueForRelease("3"); - assert(pr.directLookup.keys.length == 3); - assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); - assert(pr["2"] == TestPreparedRegistrationsGood2(true, "2")); - assert(pr["3"] == TestPreparedRegistrationsGood2(true, "3")); - - pr.queueForRelease("4"); - assert(pr.directLookup.keys.length == 3); - assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); - assert(pr["2"] == TestPreparedRegistrationsGood2(true, "2")); - assert(pr["3"] == TestPreparedRegistrationsGood2(true, "3")); - - // Test unqueueForRelease - resetData(false, true, false); - pr.unqueueForRelease("1"); - assert(pr.directLookup.keys.length == 3); - assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); - assert(pr["2"] == TestPreparedRegistrationsGood2(true, "2")); - assert(pr["3"] == TestPreparedRegistrationsGood2(false, "3")); - - pr.unqueueForRelease("2"); - assert(pr.directLookup.keys.length == 3); - assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); - assert(pr["2"] == TestPreparedRegistrationsGood2(false, "2")); - assert(pr["3"] == TestPreparedRegistrationsGood2(false, "3")); - - pr.unqueueForRelease("4"); - assert(pr.directLookup.keys.length == 3); - assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); - assert(pr["2"] == TestPreparedRegistrationsGood2(false, "2")); - assert(pr["3"] == TestPreparedRegistrationsGood2(false, "3")); - - // Test queueAllForRelease - resetData(false, true, false); - pr.queueAllForRelease(); - assert(pr["1"] == TestPreparedRegistrationsGood2(true, "1")); - assert(pr["2"] == TestPreparedRegistrationsGood2(true, "2")); - assert(pr["3"] == TestPreparedRegistrationsGood2(true, "3")); - assert(pr["4"].isNull); - - // Test clear - resetData(false, true, false); - pr.clear(); - assert(pr.directLookup.keys.length == 0); - - // Test registerIfNeeded - auto doRegister(const(char[]) sql) { return TestPreparedRegistrationsGood2(false, sql); } - pr.registerIfNeeded("1", &doRegister); - assert(pr.directLookup.keys.length == 1); - assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); - - pr.registerIfNeeded("1", &doRegister); - assert(pr.directLookup.keys.length == 1); - assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); - - pr.registerIfNeeded("2", &doRegister); - assert(pr.directLookup.keys.length == 2); - assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); - assert(pr["2"] == TestPreparedRegistrationsGood2(false, "2")); - } -} +public import mysql.unsafe.prepared; diff --git a/source/mysql/protocol/comms.d b/source/mysql/protocol/comms.d index 634df0d5..6e77dea2 100644 --- a/source/mysql/protocol/comms.d +++ b/source/mysql/protocol/comms.d @@ -28,7 +28,7 @@ import std.range; import mysql.connection; import mysql.exceptions; -import mysql.prepared; +import mysql.safe.prepared; import mysql.result; import mysql.types; diff --git a/source/mysql/protocol/extra_types.d b/source/mysql/protocol/extra_types.d index 87dadc49..522973c8 100644 --- a/source/mysql/protocol/extra_types.d +++ b/source/mysql/protocol/extra_types.d @@ -4,10 +4,10 @@ module mysql.protocol.extra_types; import std.exception; import std.variant; -import mysql.commands; +//import mysql.safe.commands; import mysql.exceptions; import mysql.protocol.sockets; -import mysql.result; +//import mysql.safe.result; import mysql.types; struct SQLValue diff --git a/source/mysql/protocol/packets.d b/source/mysql/protocol/packets.d index 8e20c10f..1e0660aa 100644 --- a/source/mysql/protocol/packets.d +++ b/source/mysql/protocol/packets.d @@ -5,7 +5,7 @@ import std.exception; import std.range; import std.string; -import mysql.commands : ColumnSpecialization, CSN; +import mysql.safe.commands : ColumnSpecialization, CSN; import mysql.exceptions; import mysql.protocol.comms; import mysql.protocol.constants; diff --git a/source/mysql/result.d b/source/mysql/result.d index 4e41c5dd..942d613f 100644 --- a/source/mysql/result.d +++ b/source/mysql/result.d @@ -1,402 +1,3 @@ -/// Structures for data received: rows and result sets (ie, a range of rows). module mysql.result; -import std.conv; -import std.exception; -import std.range; -import std.string; - -import mysql.connection; -import mysql.exceptions; -import mysql.protocol.comms; -import mysql.protocol.extra_types; -import mysql.protocol.packets; -public import mysql.types; -import std.typecons : Nullable; -import std.variant; - -/++ -A struct to represent a single row of a result set. - -Type_Mappings: $(TYPE_MAPPINGS) -+/ -/+ -The row struct is used for both 'traditional' and 'prepared' result sets. -It consists of parallel arrays of Variant and bool, with the bool array -indicating which of the result set columns are NULL. - -I have been agitating for some kind of null indicator that can be set for a -Variant without destroying its inherent type information. If this were the -case, then the bool array could disappear. -+/ -struct SafeRow -{ - import mysql.connection; - -package: - MySQLVal[] _values; // Temporarily "package" instead of "private" -private: - bool[] _nulls; - string[] _names; - -public: - @safe: - - /++ - A constructor to extract the column data from a row data packet. - - If the data for the row exceeds the server's maximum packet size, then several packets will be - sent for the row that taken together constitute a logical row data packet. The logic of the data - recovery for a Row attempts to minimize the quantity of data that is bufferred. Users can assist - in this by specifying chunked data transfer in cases where results sets can include long - column values. - - Type_Mappings: $(TYPE_MAPPINGS) - +/ - this(Connection con, ref ubyte[] packet, ResultSetHeaders rh, bool binary) - { - ctorRow(con, packet, rh, binary, _values, _nulls, _names); - } - - /++ - Simplify retrieval of a column value by index. - - To check for null, use Variant's `type` property: - `row[index].type == typeid(typeof(null))` - - Type_Mappings: $(TYPE_MAPPINGS) - - Params: i = the zero based index of the column whose value is required. - Returns: A Variant holding the column value. - +/ - ref inout(MySQLVal) opIndex(size_t i) inout - { - enforce!MYX(_nulls.length > 0, format("Cannot get column index %d. There are no columns", i)); - enforce!MYX(i < _nulls.length, format("Cannot get column index %d. The last available index is %d", i, _nulls.length-1)); - return _values[i]; - } - - /++ - Get the name of the column with specified index. - +/ - inout(string) getName(size_t index) inout - { - return _names[index]; - } - - @("getName") - debug(MYSQLN_TESTS) - unittest - { - import mysql.test.common; - import mysql.safe.commands; - mixin(scopedCn); - cn.exec("DROP TABLE IF EXISTS `row_getName`"); - cn.exec("CREATE TABLE `row_getName` (someValue INTEGER, another INTEGER) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - cn.exec("INSERT INTO `row_getName` VALUES (1, 2), (3, 4)"); - - enum sql = "SELECT another, someValue FROM `row_getName`"; - - auto rows = cn.query(sql).array; - assert(rows.length == 2); - assert(rows[0][0] == 2); - assert(rows[0][1] == 1); - assert(rows[0].getName(0) == "another"); - assert(rows[0].getName(1) == "someValue"); - assert(rows[1][0] == 4); - assert(rows[1][1] == 3); - assert(rows[1].getName(0) == "another"); - assert(rows[1].getName(1) == "someValue"); - } - - /++ - Check if a column in the result row was NULL - - Params: i = The zero based column index. - +/ - bool isNull(size_t i) const pure nothrow { return _nulls[i]; } - - /++ - Get the number of elements (columns) in this row. - +/ - @property size_t length() const pure nothrow { return _values.length; } - - ///ditto - alias opDollar = length; - - /++ - Move the content of the row into a compatible struct - - This method takes no account of NULL column values. If a column was NULL, - the corresponding Variant value would be unchanged in those cases. - - The method will throw if the type of the Variant is not implicitly - convertible to the corresponding struct member. - - Type_Mappings: $(TYPE_MAPPINGS) - - Params: - S = A struct type. - s = A ref instance of the type - +/ - void toStruct(S)(ref S s) if (is(S == struct)) - { - foreach (i, dummy; s.tupleof) - { - static if(__traits(hasMember, s.tupleof[i], "nullify") && - is(typeof(s.tupleof[i].nullify())) && is(typeof(s.tupleof[i].get))) - { - if(!_nulls[i]) - { - enforce!MYX(_values[i].convertsTo!(typeof(s.tupleof[i].get))(), - "At col "~to!string(i)~" the value is not implicitly convertible to the structure type"); - s.tupleof[i] = _values[i].get!(typeof(s.tupleof[i].get)); - } - else - s.tupleof[i].nullify(); - } - else - { - if(!_nulls[i]) - { - enforce!MYX(_values[i].convertsTo!(typeof(s.tupleof[i]))(), - "At col "~to!string(i)~" the value is not implicitly convertible to the structure type"); - s.tupleof[i] = _values[i].get!(typeof(s.tupleof[i])); - } - else - s.tupleof[i] = typeof(s.tupleof[i]).init; - } - } - } - - void show() - { - import std.stdio; - - writefln("%(%s, %)", _values); - } -} - -/// ditto -struct UnsafeRow -{ - SafeRow _safe; - alias _safe this; - Variant opIndex(size_t idx) { - return _safe[idx].asVariant; - } -} - -/// ditto -UnsafeRow unsafe(SafeRow r) @safe -{ - return Row(r); -} - -/// ditto -Nullable!UnsafeRow unsafe(Nullable!SafeRow r) @safe -{ - if(r.isNull) - return Nullable!UnsafeRow(); - return Nullable!UnsafeRow(r.get.unsafe); -} - - -SafeRow safe(UnsafeRow r) @safe -{ - return r._safe; -} - - -Nullable!SafeRow safe(Nullable!UnsafeRow r) @safe -{ - if(r.isNull) - return Nullable!SafeRow(); - return Nullable!SafeRow(r.get.safe); -} - -/++ -An $(LINK2 http://dlang.org/phobos/std_range_primitives.html#isInputRange, input range) -of Row. - -This is returned by the `mysql.commands.query` functions. - -The rows are downloaded one-at-a-time, as you iterate the range. This allows -for low memory usage, and quick access to the results as they are downloaded. -This is especially ideal in case your query results in a large number of rows. - -However, because of that, this `ResultRange` cannot offer random access or -a `length` member. If you need random access, then just like any other range, -you can simply convert this range to an array via -$(LINK2 https://dlang.org/phobos/std_array.html#array, `std.array.array()`). - -A `ResultRange` becomes invalidated (and thus cannot be used) when the server -is sent another command on the same connection. When an invalidated `ResultRange` -is used, a `mysql.exceptions.MYXInvalidatedRange` is thrown. If you need to -send the server another command, but still access these results afterwords, -you can save the results for later by converting this range to an array via -$(LINK2 https://dlang.org/phobos/std_array.html#array, `std.array.array()`). - -Type_Mappings: $(TYPE_MAPPINGS) - -Example: ---- -ResultRange oneAtATime = myConnection.query("SELECT * from myTable"); -Row[] allAtOnce = myConnection.query("SELECT * from myTable").array; ---- -+/ -struct SafeResultRange -{ -private: -@safe: - Connection _con; - ResultSetHeaders _rsh; - SafeRow _row; // current row - string[] _colNames; - size_t[string] _colNameIndicies; - ulong _numRowsFetched; - ulong _commandID; // So we can keep track of when this is invalidated - - void ensureValid() const pure - { - enforce!MYXInvalidatedRange(isValid, - "This ResultRange has been invalidated and can no longer be used."); - } - -package: - this (Connection con, ResultSetHeaders rsh, string[] colNames) - { - _con = con; - _rsh = rsh; - _colNames = colNames; - _commandID = con.lastCommandID; - popFront(); - } - -public: - /++ - Check whether the range can still be used, or has been invalidated. - - A `ResultRange` becomes invalidated (and thus cannot be used) when the server - is sent another command on the same connection. When an invalidated `ResultRange` - is used, a `mysql.exceptions.MYXInvalidatedRange` is thrown. If you need to - send the server another command, but still access these results afterwords, - you can save the results for later by converting this range to an array via - $(LINK2 https://dlang.org/phobos/std_array.html#array, `std.array.array()`). - +/ - @property bool isValid() const pure nothrow - { - return _con !is null && _commandID == _con.lastCommandID; - } - - /// Check whether there are any rows left - @property bool empty() const pure nothrow - { - if(!isValid) - return true; - - return !_con._rowsPending; - } - - /++ - Gets the current row - +/ - @property inout(SafeRow) front() pure inout - { - ensureValid(); - enforce!MYX(!empty, "Attempted 'front' on exhausted result sequence."); - return _row; - } - - /++ - Progresses to the next row of the result set - that will then be 'front' - +/ - void popFront() - { - ensureValid(); - enforce!MYX(!empty, "Attempted 'popFront' when no more rows available"); - _row = _con.getNextRow(); - _numRowsFetched++; - } - - /++ - Get the current row as an associative array by column name - - Type_Mappings: $(TYPE_MAPPINGS) - +/ - MySQLVal[string] asAA() - { - ensureValid(); - enforce!MYX(!empty, "Attempted 'front' on exhausted result sequence."); - MySQLVal[string] aa; - foreach (size_t i, string s; _colNames) - aa[s] = _row._values[i]; - return aa; - } - - /// Get the names of all the columns - @property const(string)[] colNames() const pure nothrow { return _colNames; } - - /// An AA to lookup a column's index by name - @property const(size_t[string]) colNameIndicies() pure nothrow - { - if(_colNameIndicies is null) - { - foreach(index, name; _colNames) - _colNameIndicies[name] = index; - } - - return _colNameIndicies; - } - - /// Explicitly clean up the MySQL resources and cancel pending results - void close() - out{ assert(!isValid); } - body - { - if(isValid) - _con.purgeResult(); - } - - /++ - Get the number of rows retrieved so far. - - Note that this is not neccessarlly the same as the length of the range. - +/ - @property ulong rowCount() const pure nothrow { return _numRowsFetched; } -} - -/// ditto -struct UnsafeResultRange -{ - SafeResultRange safe; - alias safe this; - inout(Row) front() inout { return inout(Row)(safe.front); } - - Variant[string] asAA() - { - ensureValid(); - enforce!MYX(!safe.empty, "Attempted 'front' on exhausted result sequence."); - Variant[string] aa; - foreach (size_t i, string s; _colNames) - aa[s] = _row._values[i].asVariant; - return aa; - } -} - -/// ditto -UnsafeResultRange unsafe(SafeResultRange r) @safe -{ - return UnsafeResultRange(r); -} - -version(MySQLSafeMode) -{ - alias Row = SafeRow; - alias ResultRange = SafeResultRange; -} -else -{ - alias Row = UnsafeRow; - alias ResultRange = UnsafeResultRange; -} +public import mysql.unsafe.result; diff --git a/source/mysql/safe/commands.d b/source/mysql/safe/commands.d index 548f745a..da439b1b 100644 --- a/source/mysql/safe/commands.d +++ b/source/mysql/safe/commands.d @@ -16,14 +16,14 @@ import std.range; import std.typecons; import std.variant; -import mysql.connection; +import mysql.safe.connection; import mysql.exceptions; -import mysql.prepared; +import mysql.safe.prepared; import mysql.protocol.comms; import mysql.protocol.constants; import mysql.protocol.extra_types; import mysql.protocol.packets; -import mysql.result; +import mysql.internal.result; import mysql.types; /// This feature is not yet implemented. It currently has no effect. @@ -238,15 +238,6 @@ ulong exec(Connection conn, ref Prepared prepared, MySQLVal[] args) return exec(conn, prepared); } -///ditto -ulong exec(Connection conn, ref BackwardCompatPrepared prepared) -{ - auto p = prepared.prepared; - auto result = exec(conn, p); - prepared._prepared = p; - return result; -} - /// Common implementation for `exec` overloads package ulong execImpl(Connection conn, ExecQueryImplInfo info) { @@ -334,13 +325,6 @@ SafeResultRange query(T...)(Connection conn, const(char[]) sql, T args) prepared.setArgs(args); return query(conn, prepared); } -///ditto -SafeResultRange query(Connection conn, const(char[]) sql, Variant[] args) @system -{ - auto prepared = conn.prepare(sql); - prepared.setArgs(args); - return query(conn, prepared); -} ///ditto SafeResultRange query(Connection conn, const(char[]) sql, MySQLVal[] args) @@ -493,15 +477,6 @@ Nullable!SafeRow queryRow(Connection conn, ref Prepared prepared, MySQLVal[] arg return queryRow(conn, prepared); } -///ditto -Nullable!SafeRow queryRow(Connection conn, ref BackwardCompatPrepared prepared) -{ - auto p = prepared.prepared; - auto result = queryRow(conn, p); - prepared._prepared = p; - return result; -} - /// Common implementation for `querySet` overloads. package Nullable!SafeRow queryRowImpl(ColumnSpecialization[] csa, Connection conn, ExecQueryImplInfo info) @@ -735,7 +710,6 @@ debug(MYSQLN_TESTS) unittest { import std.array; - import mysql.connection; import mysql.test.common; mixin(scopedCn); @@ -767,16 +741,9 @@ unittest assert(prepared.getArg(0) == 6); assert(prepared.getArg(1) == "ff"); - // exec: bcPrepared sql - auto bcPrepared = cn.prepareBackwardCompatImpl(prepareSQL); - bcPrepared.setArgs(7, "gg"); - assert(cn.exec(bcPrepared) == 1); - assert(bcPrepared.getArg(0) == 7); - assert(bcPrepared.getArg(1) == "gg"); - // Check results auto rows = cn.query("SELECT * FROM `execOverloads`").array(); - assert(rows.length == 7); + assert(rows.length == 6); assert(rows[0].length == 2); assert(rows[1].length == 2); @@ -784,7 +751,6 @@ unittest assert(rows[3].length == 2); assert(rows[4].length == 2); assert(rows[5].length == 2); - assert(rows[6].length == 2); assert(rows[0][0] == 1); assert(rows[0][1] == "aa"); @@ -798,8 +764,6 @@ unittest assert(rows[4][1] == "ee"); assert(rows[5][0] == 6); assert(rows[5][1] == "ff"); - assert(rows[6][0] == 7); - assert(rows[6][1] == "gg"); } @("queryOverloads") @@ -807,7 +771,6 @@ debug(MYSQLN_TESTS) unittest { import std.array; - import mysql.connection; import mysql.test.common; mixin(scopedCn); @@ -863,15 +826,6 @@ unittest assert(rows[0].length == 2); assert(rows[0][0] == 3); assert(rows[0][1] == "cc"); - - // BCPrepared sql - auto bcPrepared = cn.prepareBackwardCompatImpl(prepareSQL); - bcPrepared.setArgs(1, "aa"); - rows = cn.query(bcPrepared).array; - assert(rows.length == 1); - assert(rows[0].length == 2); - assert(rows[0][0] == 1); - assert(rows[0][1] == "aa"); } // Test queryRow @@ -919,15 +873,6 @@ unittest assert(row.length == 2); assert(row[0] == 3); assert(row[1] == "cc"); - - // BCPrepared sql - auto bcPrepared = cn.prepareBackwardCompatImpl(prepareSQL); - bcPrepared.setArgs(1, "aa"); - nrow = cn.queryRow(bcPrepared); - assert(!nrow.isNull); - assert(row.length == 2); - assert(row[0] == 1); - assert(row[1] == "aa"); } // Test queryRowTuple @@ -946,13 +891,6 @@ unittest cn.queryRowTuple(prepared, i, s); assert(i == 2); assert(s == "bb"); - - // BCPrepared sql - auto bcPrepared = cn.prepareBackwardCompatImpl(prepareSQL); - bcPrepared.setArgs(3, "cc"); - cn.queryRowTuple(bcPrepared, i, s); - assert(i == 3); - assert(s == "cc"); } // Test queryValue @@ -992,13 +930,5 @@ unittest assert(!value.isNull); assert(value.get.kind != MySQLVal.Kind.Null); assert(value.get == 3); - - // BCPrepared sql - auto bcPrepared = cn.prepareBackwardCompatImpl(prepareSQL); - bcPrepared.setArgs(1, "aa"); - value = cn.queryValue(bcPrepared); - assert(!value.isNull); - assert(value.get.kind != MySQLVal.Kind.Null); - assert(value.get == 1); } } diff --git a/source/mysql/safe/connection.d b/source/mysql/safe/connection.d new file mode 100644 index 00000000..db50bd42 --- /dev/null +++ b/source/mysql/safe/connection.d @@ -0,0 +1,125 @@ +module mysql.safe.connection; + +public import mysql.internal.connection; +import mysql.safe.prepared; +import mysql.safe.commands; + + +@safe: + +/++ +Submit an SQL command to the server to be compiled into a prepared statement. + +This will automatically register the prepared statement on the provided connection. +The resulting `mysql.prepared.Prepared` can then be used freely on ANY `Connection`, +as it will automatically be registered upon its first use on other connections. +Or, pass it to `Connection.register` if you prefer eager registration. + +Internally, the result of a successful outcome will be a statement handle - an ID - +for the prepared statement, a count of the parameters required for +execution of the statement, and a count of the columns that will be present +in any result set that the command generates. + +The server will then proceed to send prepared statement headers, +including parameter descriptions, and result set field descriptions, +followed by an EOF packet. + +Throws: `mysql.exceptions.MYX` if the server has a problem. ++/ +Prepared prepare(Connection conn, const(char[]) sql) +{ + auto info = conn.registerIfNeeded(sql); + return Prepared(sql, info.headers, info.numParams); +} + +/++ +Convenience function to create a prepared statement which calls a stored function. + +Be careful that your `numArgs` is correct. If it isn't, you may get a +`mysql.exceptions.MYX` with a very unclear error message. + +Throws: `mysql.exceptions.MYX` if the server has a problem. + +Params: + name = The name of the stored function. + numArgs = The number of arguments the stored procedure takes. ++/ +Prepared prepareFunction(Connection conn, string name, int numArgs) +{ + auto sql = "select " ~ name ~ preparedPlaceholderArgs(numArgs); + return prepare(conn, sql); +} + +/// +@("prepareFunction") +debug(MYSQLN_TESTS) +unittest +{ + import mysql.test.common; + import std.array; + mixin(scopedCn); + + exec(cn, `DROP FUNCTION IF EXISTS hello`); + exec(cn, ` + CREATE FUNCTION hello (s CHAR(20)) + RETURNS CHAR(50) DETERMINISTIC + RETURN CONCAT('Hello ',s,'!') + `); + + auto preparedHello = prepareFunction(cn, "hello", 1); + preparedHello.setArgs("World"); + auto rs = cn.query(preparedHello).array; + assert(rs.length == 1); + assert(rs[0][0] == "Hello World!"); +} + +/++ +Convenience function to create a prepared statement which calls a stored procedure. + +OUT parameters are currently not supported. It should generally be +possible with MySQL to present them as a result set. + +Be careful that your `numArgs` is correct. If it isn't, you may get a +`mysql.exceptions.MYX` with a very unclear error message. + +Throws: `mysql.exceptions.MYX` if the server has a problem. + +Params: + name = The name of the stored procedure. + numArgs = The number of arguments the stored procedure takes. + ++/ +Prepared prepareProcedure(Connection conn, string name, int numArgs) +{ + auto sql = "call " ~ name ~ preparedPlaceholderArgs(numArgs); + return prepare(conn, sql); +} + +/// +@("prepareProcedure") +debug(MYSQLN_TESTS) +unittest +{ + import mysql.test.common; + import mysql.test.integration; + import std.array; + mixin(scopedCn); + initBaseTestTables(cn); + + exec(cn, `DROP PROCEDURE IF EXISTS insert2`); + exec(cn, ` + CREATE PROCEDURE insert2 (IN p1 INT, IN p2 CHAR(50)) + BEGIN + INSERT INTO basetest (intcol, stringcol) VALUES(p1, p2); + END + `); + + auto preparedInsert2 = prepareProcedure(cn, "insert2", 2); + preparedInsert2.setArgs(2001, "inserted string 1"); + cn.exec(preparedInsert2); + + auto rs = query(cn, "SELECT stringcol FROM basetest WHERE intcol=2001").array; + assert(rs.length == 1); + assert(rs[0][0] == "inserted string 1"); +} + diff --git a/source/mysql/safe/package.d b/source/mysql/safe/package.d new file mode 100644 index 00000000..2bb1071c --- /dev/null +++ b/source/mysql/safe/package.d @@ -0,0 +1,20 @@ +/++ +Imports all of $(LINK2 https://github.com/mysql-d/mysql-native, mysql-native). + +This module will import all modules that use the safe API of the mysql library. +In a future version, this will become the default. ++/ +module mysql.safe; + +public import mysql.safe.commands; +public import mysql.safe.result; +public import mysql.safe.pool; +public import mysql.safe.prepared; + +// common imports +public import mysql.connection; +public import mysql.escape; +public import mysql.exceptions; +public import mysql.metadata; +public import mysql.protocol.constants : SvrCapFlags; +public import mysql.types; diff --git a/source/mysql/safe/pool.d b/source/mysql/safe/pool.d new file mode 100644 index 00000000..69df382a --- /dev/null +++ b/source/mysql/safe/pool.d @@ -0,0 +1,6 @@ +module mysql.safe.pool; + +import mysql.internal.pool; +// need to check if mysqlpool was enabled +static if(__traits(compiles, () { alias p = MySQLPoolImpl!true; })) + alias MySQLPool = MySQLPoolImpl!true; diff --git a/source/mysql/safe/prepared.d b/source/mysql/safe/prepared.d new file mode 100644 index 00000000..2c3309e9 --- /dev/null +++ b/source/mysql/safe/prepared.d @@ -0,0 +1,6 @@ +module mysql.safe.prepared; + +public import mysql.internal.prepared; +alias Prepared = SafePrepared; +alias ParameterSpecialization = SafeParameterSpecialization; +alias PSN = SafeParameterSpecialization; diff --git a/source/mysql/safe/result.d b/source/mysql/safe/result.d new file mode 100644 index 00000000..3a26d73c --- /dev/null +++ b/source/mysql/safe/result.d @@ -0,0 +1,6 @@ +module mysql.safe.result; + +public import mysql.internal.result; + +alias Row = SafeRow; +alias ResultRange = SafeResultRange; diff --git a/source/mysql/test/common.d b/source/mysql/test/common.d index 0b751226..09b7b974 100644 --- a/source/mysql/test/common.d +++ b/source/mysql/test/common.d @@ -22,7 +22,7 @@ import mysql.connection; import mysql.exceptions; import mysql.protocol.extra_types; import mysql.protocol.sockets; -import mysql.result; +import mysql.safe.result; import mysql.types; /+ diff --git a/source/mysql/test/integration.d b/source/mysql/test/integration.d index 78cdb776..1b6ab009 100644 --- a/source/mysql/test/integration.d +++ b/source/mysql/test/integration.d @@ -14,14 +14,14 @@ import std.typecons; import std.variant; import mysql.safe.commands; -import mysql.connection; +import mysql.safe.connection; import mysql.exceptions; import mysql.metadata; import mysql.protocol.constants; import mysql.protocol.extra_types; import mysql.protocol.packets; import mysql.protocol.sockets; -import mysql.result; +import mysql.safe.result; import mysql.test.common; @safe: @@ -115,7 +115,7 @@ debug(MYSQLN_TESTS) debug(MYSQLN_TESTS) unittest { - import mysql.prepared; + import mysql.safe.prepared; struct X { @@ -573,7 +573,7 @@ https://github.com/simendsjo/mysqln debug(MYSQLN_TESTS) unittest { - import mysql.prepared; + import mysql.safe.prepared; mixin(scopedCn); cn.exec("DROP TABLE IF EXISTS manytypes"); cn.exec( "CREATE TABLE manytypes (" @@ -584,9 +584,9 @@ unittest ~")"); //DataSet ds; - SafeRow[] rs; + Row[] rs; //Table tbl; - SafeRow row; + Row row; Prepared stmt; // Index out of bounds throws @@ -851,7 +851,7 @@ unittest debug(MYSQLN_TESTS) unittest { - import mysql.prepared; + import mysql.safe.prepared; mixin(scopedCn); void assertBasicTests(T, U)(string sqlType, U[] values ...) @safe @@ -967,14 +967,14 @@ unittest debug(MYSQLN_TESTS) unittest { - import mysql.prepared; + import mysql.safe.prepared; mixin(scopedCn); auto stmt = cn.prepare( "SELECT * FROM information_schema.character_sets"~ " WHERE CHARACTER_SET_NAME=?"); auto val = "utf8"; stmt.setArg(0, val); - auto row = cn.queryRow(stmt).get(SafeRow.init); + auto row = cn.queryRow(stmt).get(Row.init); //assert(row.length == 4); assert(row.length == 4); assert(row[0] == "utf8"); @@ -987,7 +987,7 @@ unittest debug(MYSQLN_TESTS) unittest { - import mysql.prepared; + import mysql.safe.prepared; mixin(scopedCn); cn.exec("DROP TABLE IF EXISTS `coupleTypes`"); @@ -1005,7 +1005,7 @@ unittest { // Test query - SafeResultRange rseq = cn.query(selectSQL); + ResultRange rseq = cn.query(selectSQL); assert(!rseq.empty); assert(rseq.front.length == 2); assert(rseq.front[0] == 11); @@ -1026,7 +1026,7 @@ unittest { // Test prepared query - SafeResultRange rseq = cn.query(prepared); + ResultRange rseq = cn.query(prepared); assert(!rseq.empty); assert(rseq.front.length == 2); assert(rseq.front[0] == 11); @@ -1047,7 +1047,7 @@ unittest { // Test reusing the same ResultRange - SafeResultRange rseq = cn.query(selectSQL); + ResultRange rseq = cn.query(selectSQL); assert(!rseq.empty); rseq.each(); assert(rseq.empty); @@ -1058,7 +1058,7 @@ unittest } { - Nullable!SafeRow nullableRow; + Nullable!Row nullableRow; // Test queryRow nullableRow = cn.queryRow(selectSQL); @@ -1129,7 +1129,7 @@ unittest { // Issue new command before old command was purged // Ensure old result set is auto-purged and invalidated. - SafeResultRange rseq1 = cn.query(selectSQL); + ResultRange rseq1 = cn.query(selectSQL); rseq1.popFront(); assert(!rseq1.empty); assert(rseq1.isValid); @@ -1142,7 +1142,7 @@ unittest { // Test using outdated ResultRange - SafeResultRange rseq1 = cn.query(selectSQL); + ResultRange rseq1 = cn.query(selectSQL); rseq1.popFront(); assert(!rseq1.empty); assert(rseq1.front[0] == 22); @@ -1154,7 +1154,7 @@ unittest assertThrown!MYXInvalidatedRange(rseq1.popFront()); assertThrown!MYXInvalidatedRange(rseq1.asAA()); - SafeResultRange rseq2 = cn.query(selectBackwardsSQL); + ResultRange rseq2 = cn.query(selectBackwardsSQL); assert(!rseq2.empty); assert(rseq2.front.length == 2); assert(rseq2.front[0] == "ccc"); diff --git a/source/mysql/test/regression.d b/source/mysql/test/regression.d index 2bb5b6af..e5bf24ac 100644 --- a/source/mysql/test/regression.d +++ b/source/mysql/test/regression.d @@ -19,12 +19,12 @@ import std.string; import std.traits; import std.variant; -import mysql.commands; +import mysql.safe.commands; import mysql.connection; import mysql.exceptions; import mysql.prepared; import mysql.protocol.sockets; -import mysql.result; +import mysql.safe.result; import mysql.test.common; // Issue #24: Driver doesn't like BIT @@ -294,8 +294,8 @@ version(Have_vibe_core) debug(MYSQLN_TESTS) unittest { - import mysql.commands; - import mysql.pool; + import mysql.safe.commands; + import mysql.safe.pool; int count=0; auto pool = new MySQLPool(testConnectionStr); diff --git a/source/mysql/unsafe/commands.d b/source/mysql/unsafe/commands.d index 615846ca..fd475722 100644 --- a/source/mysql/unsafe/commands.d +++ b/source/mysql/unsafe/commands.d @@ -17,14 +17,14 @@ import std.range; import std.typecons; import std.variant; -import mysql.connection; +import mysql.unsafe.connection; import mysql.exceptions; -import mysql.prepared; +import mysql.unsafe.prepared; import mysql.protocol.comms; import mysql.protocol.constants; import mysql.protocol.extra_types; import mysql.protocol.packets; -import mysql.result; +import mysql.unsafe.result; import mysql.types; alias ColumnSpecialization = SC.ColumnSpecialization; @@ -186,6 +186,16 @@ ulong exec(Connection conn, ref Prepared prepared, Variant[] args) return exec(conn, prepared); } +///ditto +ulong exec(Connection conn, ref BackwardCompatPrepared prepared) +{ + auto p = prepared.prepared; + auto result = exec(conn, p); + prepared._prepared = p; + return result; +} + + /++ Execute an SQL SELECT command or prepared statement. @@ -265,7 +275,7 @@ UnsafeResultRange query(Connection conn, const(char[]) sql, Variant[] args) return query(conn, prepared); } ///ditto -UnsafeResultRange query(Connection conn, ref Prepared prepared) +UnsafeResultRange query(Connection conn, ref Prepared prepared) @safe { return SC.query(conn, prepared).unsafe; } @@ -355,7 +365,7 @@ delegate. csa = An optional array of `ColumnSpecialization` structs. If you need to use this with a prepared statement, please use `mysql.prepared.Prepared.columnSpecials`. +/ -Nullable!UnsafeRow queryRow(Connection conn, const(char[]) sql, ColumnSpecialization[] csa = null) +Nullable!UnsafeRow queryRow(Connection conn, const(char[]) sql, ColumnSpecialization[] csa = null) @safe { return SC.queryRow(conn, sql, csa).unsafe; } @@ -373,7 +383,7 @@ Nullable!UnsafeRow queryRow(Connection conn, const(char[]) sql, Variant[] args) return queryRow(conn, prepared); } ///ditto -Nullable!UnsafeRow queryRow(Connection conn, ref Prepared prepared) +Nullable!UnsafeRow queryRow(Connection conn, ref Prepared prepared) @safe { return SC.queryRow(conn, prepared).unsafe; } @@ -391,7 +401,7 @@ Nullable!UnsafeRow queryRow(Connection conn, ref Prepared prepared, Variant[] ar } ///ditto -Nullable!UnsafeRow queryRow(Connection conn, ref BackwardCompatPrepared prepared) +Nullable!UnsafeRow queryRow(Connection conn, ref BackwardCompatPrepared prepared) @safe { auto p = prepared.prepared; auto result = queryRow(conn, p); @@ -555,7 +565,6 @@ debug(MYSQLN_TESTS) unittest { import std.array; - import mysql.connection; import mysql.test.common; mixin(scopedCn); @@ -627,7 +636,6 @@ debug(MYSQLN_TESTS) unittest { import std.array; - import mysql.connection; import mysql.test.common; mixin(scopedCn); diff --git a/source/mysql/unsafe/connection.d b/source/mysql/unsafe/connection.d new file mode 100644 index 00000000..f2845f1a --- /dev/null +++ b/source/mysql/unsafe/connection.d @@ -0,0 +1,147 @@ +module mysql.unsafe.connection; + +import mysql.unsafe.prepared; +import mysql.unsafe.commands; +public import mysql.internal.connection; +private import CS = mysql.safe.connection; + +Prepared prepare(Connection conn, const(char[]) sql) @safe +{ + return CS.prepare(conn, sql).unsafe; +} + +Prepared prepareFunction(Connection conn, string name, int numArgs) @safe +{ + return CS.prepareFunction(conn, name, numArgs).unsafe; +} + +Prepared prepareProcedure(Connection conn, string name, int numArgs) @safe +{ + return CS.prepareProcedure(conn, name, numArgs).unsafe; +} + +/++ +This function is provided ONLY as a temporary aid in upgrading to mysql-native v2.0.0. + +See `BackwardCompatPrepared` for more info. ++/ +deprecated("This is provided ONLY as a temporary aid in upgrading to mysql-native v2.0.0. You should migrate from this to the Prepared-compatible exec/query overloads in 'mysql.commands'.") +BackwardCompatPrepared prepareBackwardCompat(Connection conn, const(char[]) sql) +{ + return prepareBackwardCompatImpl(conn, sql); +} + +/// Allow mysql-native tests to get around the deprecation message +package(mysql) BackwardCompatPrepared prepareBackwardCompatImpl(Connection conn, const(char[]) sql) +{ + return BackwardCompatPrepared(conn, prepare(conn, sql)); +} + +/++ +This is a wrapper over `mysql.prepared.Prepared`, provided ONLY as a +temporary aid in upgrading to mysql-native v2.0.0 and its +new connection-independent model of prepared statements. See the +$(LINK2 https://github.com/mysql-d/mysql-native/blob/master/MIGRATING_TO_V2.md, migration guide) +for more info. + +In most cases, this layer shouldn't even be needed. But if you have many +lines of code making calls to exec/query the same prepared statement, +then this may be helpful. + +To use this temporary compatability layer, change instances of: + +--- +auto stmt = conn.prepare(...); +--- + +to this: + +--- +auto stmt = conn.prepareBackwardCompat(...); +--- + +And then your prepared statement should work as before. + +BUT DO NOT LEAVE IT LIKE THIS! Ultimately, you should update +your prepared statement code to the mysql-native v2.0.0 API, by changing +instances of: + +--- +stmt.exec() +stmt.query() +stmt.queryRow() +stmt.queryRowTuple(outputArgs...) +stmt.queryValue() +--- + +to this: + +--- +conn.exec(stmt) +conn.query(stmt) +conn.queryRow(stmt) +conn.queryRowTuple(stmt, outputArgs...) +conn.queryValue(stmt) +--- + +Both of the above syntaxes can be used with a `BackwardCompatPrepared` +(the `Connection` passed directly to `mysql.commands.exec`/`mysql.commands.query` +will override the one embedded associated with your `BackwardCompatPrepared`). + +Once all of your code is updated, you can change `prepareBackwardCompat` +back to `prepare` again, and your upgrade will be complete. ++/ +struct BackwardCompatPrepared +{ + import std.variant; + import mysql.unsafe.result; + import std.typecons; + + private Connection _conn; + Prepared _prepared; + + /// Access underlying `Prepared` + @property Prepared prepared() @safe { return _prepared; } + + alias _prepared this; + + /++ + This function is provided ONLY as a temporary aid in upgrading to mysql-native v2.0.0. + + See `BackwardCompatPrepared` for more info. + +/ + deprecated("Change 'preparedStmt.exec()' to 'conn.exec(preparedStmt)'") + ulong exec() @safe + { + return .exec(_conn, _prepared); + } + + ///ditto + deprecated("Change 'preparedStmt.query()' to 'conn.query(preparedStmt)'") + ResultRange query() @safe + { + return .query(_conn, _prepared); + } + + ///ditto + deprecated("Change 'preparedStmt.queryRow()' to 'conn.queryRow(preparedStmt)'") + Nullable!Row queryRow() @safe + { + return .queryRow(_conn, _prepared); + } + + ///ditto + deprecated("Change 'preparedStmt.queryRowTuple(outArgs...)' to 'conn.queryRowTuple(preparedStmt, outArgs...)'") + void queryRowTuple(T...)(ref T args) if(T.length == 0 || !is(T[0] : Connection)) + { + return .queryRowTuple(_conn, _prepared, args); + } + + ///ditto + deprecated("Change 'preparedStmt.queryValue()' to 'conn.queryValue(preparedStmt)'") + Nullable!Variant queryValue() @system + { + return .queryValue(_conn, _prepared); + } +} + diff --git a/source/mysql/unsafe/package.d b/source/mysql/unsafe/package.d new file mode 100644 index 00000000..686bccc7 --- /dev/null +++ b/source/mysql/unsafe/package.d @@ -0,0 +1,20 @@ +/++ +Imports all of $(LINK2 https://github.com/mysql-d/mysql-native, mysql-native). + +This module will import all modules that use the unsafe API of the mysql +library. Please import `mysql.safe` for the safe version. ++/ +module mysql.unsafe; + +public import mysql.unsafe.commands; +public import mysql.unsafe.result; +public import mysql.unsafe.pool; +public import mysql.unsafe.prepared; + +// common imports +public import mysql.connection; +public import mysql.escape; +public import mysql.exceptions; +public import mysql.metadata; +public import mysql.protocol.constants : SvrCapFlags; +public import mysql.types; diff --git a/source/mysql/unsafe/pool.d b/source/mysql/unsafe/pool.d new file mode 100644 index 00000000..698cd3d6 --- /dev/null +++ b/source/mysql/unsafe/pool.d @@ -0,0 +1,6 @@ +module mysql.unsafe.pool; + +import mysql.internal.pool; +// need to check if mysqlpool was enabled +static if(__traits(compiles, () { alias p = MySQLPoolImpl!false; })) + alias MySQLPool = MySQLPoolImpl!false; diff --git a/source/mysql/unsafe/prepared.d b/source/mysql/unsafe/prepared.d new file mode 100644 index 00000000..9882868a --- /dev/null +++ b/source/mysql/unsafe/prepared.d @@ -0,0 +1,6 @@ +module mysql.unsafe.prepared; + +public import mysql.internal.prepared; +alias Prepared = UnsafePrepared; +alias ParameterSpecialization = UnsafeParameterSpecialization; +alias PSN = UnsafeParameterSpecialization; diff --git a/source/mysql/unsafe/result.d b/source/mysql/unsafe/result.d new file mode 100644 index 00000000..870acd78 --- /dev/null +++ b/source/mysql/unsafe/result.d @@ -0,0 +1,6 @@ +module mysql.unsafe.result; + +public import mysql.internal.result; + +alias Row = UnsafeRow; +alias ResultRange = UnsafeResultRange; From 78d9891061bb1559b9c5bfa7ec9354689e869d6d Mon Sep 17 00:00:00 2001 From: Steven Schveighoffer Date: Fri, 21 Feb 2020 11:38:50 -0500 Subject: [PATCH 11/14] The big doc update. Also cleaned up a bunch of imports and little things I saw while documenting. --- SAFE_MIGRATION.md | 124 +++++++++++++++---- build-docs | 7 +- ddoc/macros.ddoc | 5 +- source/mysql/commands.d | 6 +- source/mysql/connection.d | 18 +++ source/mysql/escape.d | 5 +- source/mysql/{internal => impl}/connection.d | 18 ++- source/mysql/{internal => impl}/pool.d | 38 ++++-- source/mysql/{internal => impl}/prepared.d | 106 ++++++++++------ source/mysql/{internal => impl}/result.d | 97 ++++++++++----- source/mysql/package.d | 9 +- source/mysql/pool.d | 13 ++ source/mysql/prepared.d | 9 ++ source/mysql/result.d | 16 +++ source/mysql/safe/commands.d | 98 ++++++++------- source/mysql/safe/connection.d | 19 ++- source/mysql/safe/package.d | 4 +- source/mysql/safe/pool.d | 16 ++- source/mysql/safe/prepared.d | 18 ++- source/mysql/safe/result.d | 15 ++- source/mysql/types.d | 115 ++++++++++++++++- source/mysql/unsafe/commands.d | 34 ++--- source/mysql/unsafe/connection.d | 36 ++++-- source/mysql/unsafe/package.d | 4 +- source/mysql/unsafe/pool.d | 16 ++- source/mysql/unsafe/prepared.d | 18 ++- source/mysql/unsafe/result.d | 15 ++- 27 files changed, 682 insertions(+), 197 deletions(-) rename source/mysql/{internal => impl}/connection.d (98%) rename source/mysql/{internal => impl}/pool.d (94%) rename source/mysql/{internal => impl}/prepared.d (89%) rename source/mysql/{internal => impl}/result.d (74%) diff --git a/SAFE_MIGRATION.md b/SAFE_MIGRATION.md index cba721e5..4c3f3760 100644 --- a/SAFE_MIGRATION.md +++ b/SAFE_MIGRATION.md @@ -1,8 +1,8 @@ # Migrating code to use the @safe API of mysql-native -This document describes how mysql-native is migrating to an all-@safe API and library, and how you can migrate your existing code to the new version. +Starting with version 3.1.0, mysql-native is transitioning to using a fully `@safe` API. This document describes how mysql-native is migrating, and how you can migrate your existing code to the new version. -First, note that the latest version of mysql, while it supports a safe API, is defaulted to supporting the original unsafe API. We highly recommend reading and following the recommendations in this document so you can start using the safe version. +First, note that the latest version of mysql, while it supports a safe API, is defaulted to supporting the original unsafe API. We highly recommend reading and following the recommendations in this document so you can start using the safe version and be prepared for future changes that will deprecate and remove the unsafe API. ## Why Safe? @@ -12,6 +12,14 @@ Since mysql-native is intended to be a key component of servers that are on the In other words, the world wants memory safe code, and libraries that provide safe interfaces and guarantees will be much more appealing. It's just not acceptable any more for the components of major development projects to be careless about memory safety. +## Roadmap + +The intended roadmap for migrating to a safe API is the following: + +* v3.1.0 - In this version, the `safe` and `unsafe` packages were introduced, providing a way to specify exactly which API you want to use. The default modules and packges in this version import the `unsafe` versions of the API to maintain full backwards compatibility. +* v4.0.0 - In this version, the `unsafe` versions of the API will be deprecated, meaning you can still use them, but you will get warnings. In addition, the default modules and packages will import the `safe` API. +* Future version (possibly v5.0.0) - In this version, the `unsafe` API will be completely removed, and the `safe` modules now simply publicly import the standard modules. The `mysql.impl` package will be removed. + ## The Major Changes mysql-native until now used the Phobos type `std.variant.Variant` to hold data who's type was unknown at compile time. Unfortunately, since `Variant` can hold *any* type, it must default to having a `@system` postblit/copy constructor, and a `@system` destructor. This means that just copying a `Variant`, passing it as a function parameter, or returning it from a function makes the function doing such things `@system`. This meant that we needed to move from `Variant` to a new type that allows only safe usages, but still maintained the ability to decide types at runtime. @@ -22,21 +30,31 @@ The module `mysql.types` now contains a new type called `MySQLVal`, which should ### The safe/unsafe API -In some cases, fixing memory safety in mysql-native was as simple as adding a `@safe` tag to the module or functions in the module. These functions should work just as before, but are now callable from `@safe` code. +In some cases, fixing memory safety in mysql-native was as simple as adding a `@safe` tag to the module or functions in the module. These functions and modules should work just as before, but are now callable from `@safe` code. -But for the rest, to achieve full backwards compatibility, we have divided the API into two major sections -- safe and unsafe. The package `mysql.safe` will import all the safe versions of the API, the package `mysql.unsafe` will import the unsafe versions. If you import `mysql`, it will currently point at the unsafe version for backwards compatibility. +But for the rest, to achieve full backwards compatibility, we have divided the API into two major sections -- safe and unsafe. The package `mysql.safe` will import all the safe versions of the API, the package `mysql.unsafe` will import the unsafe versions. If you import `mysql`, it will currently point at the unsafe version for backwards compatibility (see [Roadmap](#Roadmap) for details on how this will change). -The following modules have been split into mysql.safe.*modname* and mysql.unsafe.*modname*. Importing mysql.*modname* will import the unsafe version for backwards compatibility. -* module mysql.commands -* module mysql.pool -* module mysql.result -* module mysql.prepared -* module mysql.connection +The following modules have been split into mysql.safe.*modname* and mysql.unsafe.*modname*. Importing mysql.*modname* will currently import the unsafe version for backwards compatibility. +* `mysql.commands` +* `mysql.pool` +* `mysql.result` +* `mysql.prepared` +* `mysql.connection` Each of these modules in unsafe mode provides the same API as the previous version of mysql. The safe version provides aliases to the original type names for the safe versions of types, and also provides the same functions as before that can be called via safe code. The one exception is in `mysql.safe.commands`, where some functions were for the deprecated `BackwardCompatPrepared`, which will eventually be removed. If you are currently importing any of the above modules directly, or importing the `mysql` package, a first step to migration is to use the `mysql.safe` package. From there you will find that almost everything works exactly the same. +In addition to these two new packages, we have introduced a package called `mysql.impl`. This package contains the common implementations of the `safe` and `unsafe` modules, and should NOT be directly imported ever. These modules are documented simply because that is where the code lives. But in the version of mysql that removes the `unsafe` API, this package will be removed. You should always use the `unsafe` or `safe` packages instead, which generally publicly import the `impl` modules anyway. + +#### Importing both safe and unsafe + +It is possible to import some modules using the safe package, and some using the unsafe package in the case where you are gradually migrating to safe versions of your code. However, you should not import the same module from both safe and unsafe packages, as there will be naming conflicts. + +For example, if you import from `mysql.safe.result` and `mysql.unsafe.result`, the alias for `Row` will be tied to both `UnsafeRow` and `SafeRow`, resulting in a compilation ambiguity. + +But it is definitely possible to import `mysql.unsafe.result` and `mysql.safe.commands`. You may need to use the `safe` or `unsafe` conversion methods on the types to make your code function as desired. See details later on these conversion methods. + ### Migrating from Variant to MySQLVal The module `mysql.types` has been amended to contain the `MySQLVal` type. This type can hold any value type that MySQL supported originally, or a const pointer to such a type (for the purposes of prepared statements), or the value `null`. This is now the type used for all parameters to `query` and `exec` (in the safe API). The `mysql.types` import also provides compatibility shims with `Variant` such as `coerce`, `convertsTo`, `type`, `peek`, and `get` (See the documentation for [Variant](https://dlang.org/phobos/std_variant.html#.VariantN)). @@ -45,7 +63,23 @@ You can examine all the benefits of `TaggedAlgebraic` [here](https://vibed.org/a One pitfall of this migration has to do with `Variant`'s ability to represent *any* type -- including `MySQLVal`! If you have declared a variable of type `Variant`, and assign it to a `MySQLVal` result from a row or a query, it will compile, but it will NOT do what you are expecting. This will fail at runtime most likely. It is recommended before switching to the safe API to change those types to `MySQLVal` or use `auto` if possible. -The `mysql.types` module also contains a compatibility function `asVariant`, which can be used when you want to use the safe API but absolutely need a `Variant` from a `MySQLVal`. The opposite conversion is implemented, but not exposed publically since there is no compatibility issue for existing code. +Example: +```D +import mysql.safe; + +Variant v = connection.queryValue("SELECT 1 AS `somevar`"); +``` + +This will compile and run, and the resulting variable `v` will be a `MySQLVal` typed as a `MySQLVal.Kind.Int` wrapped in a `Variant`. This is not what you want. In order to fix this, you should either re-type `v` as `MySQLVal` (or use `auto`) or use the `asVariant` function included in `mysql.types`: + +```D +import mysql.safe; + +// preferred +MySQLVal v = connection.queryValue("SELECT 1 AS `somevar`"); +// if necessary +Variant v2 = connection.queryValue("SELECT 1 AS `somevar`").asVariant; +``` One important thing to note is that the internals of mysql-native have all been switched to using `MySQLVal` instead of `Variant`. Only at the shallow API level is `Variant` used to provide the backwards compatible API. So if you do not switch, you will pay the penalty of having the library first construct a `MySQLVal` and then convert that to a `Variant` (or vice versa). @@ -53,11 +87,50 @@ One important thing to note is that the internals of mysql-native have all been These two types were tied greatly to `Variant`. As such, they have been rewritten into `SafeRow` and `SafeResultRange` which use `MySQLVal` instead. Thin compatibility wrappers of `UnsafeRow` and `UnsafeResultRange` are available as well, which will convert the values to and from `Variant` as needed. Depending on which API you import `safe` or `unsafe`, these items are aliased to `Row` and `ResultRange` for source compatibility. -For this reason, you should not import both the `safe` and `unsafe` API, as you will get ambiguity errors. - However, each of these structures provides `unsafe` and `safe` conversion functions to convert between the two if absolutely necessary. In fact, most of the unsafe API calls that return an `UnsafeRow` or `UnsafeResultRange` are actually `@safe`, since the underlying implementation uses `MySQLVal`. It only becomes unsafe when you try to access a column as a `Variant`. -TODO: some examples needed +The following example should compile with both `mysql.safe` and `mysql.unsafe`, but simply use `Variant` or `MySQLVal` as needed: +```D +import mysql; + +// assume a database table named 'mapping' with a string 'name' and int 'value' +int getMapping(Connection conn, string name) +{ + Row r = conn.queryRow("SELECT * FROM mapping WHERE name = ?", name); + assert(r[0].type == typeid(int)); + return r[0].get!int; +} +``` +While the safe version provides drop-in compatibility, it is recommended to switch to safe operations instead: + +```D +import mysql.safe; + +int getMapping(Connection conn, string name) @safe +{ + Row r = conn.queryRow("SELECT * FROM mapping WHERE name = ?", name); + //assert(r[0].type == typeid(int)); // this would work, but is @system + assert(r[0].kind == MySQLVal.Kind.Int); + return r[0].get!int; +} +``` + +In cases where current code requires the use of `Variant`, you can still use the safe API, and just do a conversion where needed: + +```D +import mysql.safe; + +struct EstablishedStruct +{ + Variant value; + int id; + void fetchFromDatabase(Connection conn) + { + // all safe calls except asVariant + value conn.queryValue("SELECT value FROM theTable WHERE id = ?", id).asVariant; + } +} +``` ### Prepared @@ -65,24 +138,33 @@ The `Prepared` struct contained support for setting/getting `Variant` parameters The `mysql.safe.prepared` module will alias `Prepared` as the safe version, and the `mysql.unsafe.prepared` module will alias `Prepared` as the unsafe version. +One other aspect of `Prepared` that is different in the two versions is the `ParameterSpecialization` data. There are now two different such structs, a `SafeParameterSpecialization` and an `UnsafeParameterSpecialization`. The only difference between these two is the `chunkDelegate` being `@safe` or `@system`. If you do not use the `chunkDelegate`, or your delegate is actually `@safe`, then you should opt for the `@safe` API. + ### Connection -The Connection class itself has not changed at all, except to add @safe for everything. However, the `mysql.connection` module contained the functions to generate `Prepared` structs. +The Connection class itself has not changed at all, except to add @safe attributes for all methods. However, the `mysql.connection` module contained the functions to generate `Prepared` structs. The `BackwardsCompatPrepared` struct defined in the original `mysql.connection` module is only available in the unsafe package. ### MySQLPool -`MySQLPool` has been factored into a templated type that has either a fully safe or partly safe API. The only public facing unsafe part was the user-supplied callback function to be called on every connection creation (which therefore makes `lockConnection` unsafe). The unsafe version continues to use such a callback method (and is explicitly marked `@system`), whereas the safe version requires a `@safe` callback. If you do not use this callback mechanism, it is highly recommended that you use the safe API for the pool, as there is no actual difference between the two at that point. It's also very likely that your callback actually is `@safe`, even if you do use one. +`MySQLPool` has been factored into a templated type that has either a fully safe or partly safe API. The only public facing unsafe part was the user-supplied callback function to be called on every connection creation (which therefore made `lockConnection` unsafe). The unsafe version continues to use such a callback method (and is explicitly marked `@system`), whereas the safe version requires a `@safe` callback. + +If you do not use this callback mechanism, it is highly recommended that you use the safe API for the pool, as there is no actual difference between the two at that point. It's also very likely that your callback actually is `@safe`, even if you do use one. ### The commands module -As previously mentioned, the `mysql.commands` module has been factored into 2 versions, a safe and unsafe version. The only differences between these two are where `Variant` is concerned. All query and exec functions that accepted `Variant` explicitly have been reimplemented in the safe version to accept `MySQLVal`. All functions that returned `Variant` have been reimplemented to return `MySQLVal`. All functions that do not deal with `Variant` are moved to the safe API, and aliased in the unsafe API. This means, as long as you do not use `Variant` explicitly, you should be able to switch over to the safe version of the API without changing your code. +The `mysql.commands` module has been factored into 2 versions, a safe and unsafe version. The only differences between these two are where `Variant` is concerned. All query and exec functions that accepted `Variant` explicitly have been reimplemented in the safe version to accept `MySQLVal`. All functions that return `Variant`, `Row` or `ResultRange` have been reimplemented to return `MySQLVal`, `SafeRow`, or `SafeResultRange` respectively. All functions that do not deal with these types are moved to the safe API, and aliased in the unsafe API. This means, as long as you do not use `Variant` explicitly, you should be able to switch over to the safe version of the API without changing your code. -TODO: some examples needed +Even in cases where you elect to defer updating code, you can still import the `safe` API, and use `unsafe` conversion functions to keep existing code working. In most cases, this will not be necessary as the API is kept as similar as possible. -## Future versions +## Rcommended Transition Method -The next major version of mysql-native will swap the default package imports to the safe API. In addition, all unsafe functions and types will be marked deprecated. +We recommend following these steps to transition. In most cases, you should see very little breakage of code: -In a future major version (not necessarily the one after the above version), the unsafe API will be completely removed, and the safe API will take the place of the default modules. The explicit `mysql.safe` packages will remain for backwards compatibility. At this time, all uses of `Variant` will be gone. +1. Adjust your imports to import the safe versions of mysql moduels. If you import the `mysql` package, instead import the `mysql.safe` package. If you import any of the individual modules listed in the [API](#The safe/unsafe API) section, use the `mysql.safe.modulename` equivalent instead. +2. Adjust any explicit uses of `Variant` to `MySQLVal` or use `auto` for type inference. Remember that variables typed as `Variant` explicitly will consume `MySQLVal`, so you may not get compiler errors for these, but you will certainly get runtime errors. +3. If there are cases where you cannot stop using `Variant`, use the `asVariant` compatibility shim. +4. Adjust uses of `Variant`'s methods to use the `TaggedAlgebraic` versions. Most important is usage of the `kind` member, as comparing two `TypeInfo` objects is currently `@system`. +5. `MySQLVal` provides a richer experience of type forwarding, so you may be able to relax some of your code that is concerned with first fetching the concrete type from the `Variant`. Notably, `MySQLVal` can access directly members of any of the `std.datetime` types, such as `year`, `month`, or `day`. +6. Report ANY issues with compatibility or bugs to the issue tracker so we may deal with them ASAP. Our intention is to have you be able to use v3.1.0 without having to adjust any code that worked with 3.0.0. diff --git a/build-docs b/build-docs index 2eb1f424..8d99abb3 100755 --- a/build-docs +++ b/build-docs @@ -8,8 +8,11 @@ # DMD 2.086.0 and up cause errors when building the docs because # gen-package-version and Scriptlike need updated. -rdmd --build-only -c -Isource -Dddocs_tmp -X -Xfdocs/docs.json -version=MySQLDocs --force source/mysql/package.d -rm -rf docs_tmp +# Need taggedalgebraic +rm -rf ta +git clone https://github.com/s-ludwig/taggedalgebraic.git ta +rdmd --build-only -c -Isource -Ita/source -Dddocs_tmp -X -Xfdocs/docs.json -version=MySQLDocs --force source/mysql/package.d +rm -rf docs_tmp ta rm source/mysql/package.o dub --version diff --git a/ddoc/macros.ddoc b/ddoc/macros.ddoc index 25c29fd5..3c94997b 100644 --- a/ddoc/macros.ddoc +++ b/ddoc/macros.ddoc @@ -6,9 +6,10 @@ Macros: DOLLAR = $ COLON = : EM = $(B $(I $0) ) - + TYPE_MAPPINGS = See the $(LINK2 $(DDOX_ROOT_DIR)/mysql.html, MySQL/D Type Mappings tables) - + SAFE_MIGRATION = See the $(LINK2 https://github.com/mysql-d/mysql-native/blob/master/SAFE_MIGRATION.md, Safe Migration) document for more details. + STD_DATETIME_DATE = $(D_INLINECODE $(LINK2 https://dlang.org/phobos/std_datetime_date.html#$0, $0)) STD_EXCEPTION = $(D_INLINECODE $(LINK2 https://dlang.org/phobos/std_exception.html#$0, $0)) STD_FILE = $(D_INLINECODE $(LINK2 https://dlang.org/phobos/std_file.html#$0, $0)) diff --git a/source/mysql/commands.d b/source/mysql/commands.d index 4bbb3dd4..b9f48203 100644 --- a/source/mysql/commands.d +++ b/source/mysql/commands.d @@ -1,6 +1,6 @@ /++ -This module imports `mysql.unsafe.commands`, which provides the Variant-based -interface to mysql. In the future, this will switch to importing the +This module publicly imports `mysql.unsafe.commands`, which provides the +Variant-based interface to mysql. In the future, this will switch to importing the `mysql.safe.commands`, which provides the @safe interface to mysql. Please see those two modules for documentation on the functions provided. It is highly recommended to import `mysql.safe.commands` and not the unsafe commands, as @@ -8,6 +8,8 @@ that is the future for mysql-native. In the far future, the unsafe version will be deprecated and removed, and the safe version moved to this location. + +$(SAFE_MIGRATION) +/ module mysql.commands; public import mysql.unsafe.commands; diff --git a/source/mysql/connection.d b/source/mysql/connection.d index 7452813c..ae64d751 100644 --- a/source/mysql/connection.d +++ b/source/mysql/connection.d @@ -1,3 +1,21 @@ +/++ +This module publicly imports `mysql.unsafe.connection`, which provides +backwards compatible functions for connecting to a MySQL/MariaDB server. + +It is recommended instead to import `mysql.safe.connection`, which provides +@safe-only mechanisms to connect to a database. + +Note that the common pieces of the connection are documented and currently +reside in `mysql.impl.connection`. The safe and unsafe portions of the API +reside in `mysql.unsafe.connection` and `mysql.safe.connection` respectively. +Please see these modules for information on using a MySQL `Connection` object. + +In the future, this will migrate to importing `mysql.safe.connection`. In the +far future, the unsafe version will be deprecated and removed, and the safe +version moved to this location. + +$(SAFE_MIGRATION) ++/ module mysql.connection; public import mysql.unsafe.connection; diff --git a/source/mysql/escape.d b/source/mysql/escape.d index 6b58812d..256a543a 100644 --- a/source/mysql/escape.d +++ b/source/mysql/escape.d @@ -40,6 +40,9 @@ properly escaped all using the buffer that formattedWrite provides. Params: Input = (Template Param) Type of the input + +Note: + The delegate is expected to be @safe as of version 3.1.0. +/ struct MysqlEscape ( Input ) { @@ -65,7 +68,7 @@ MysqlEscape!(T) mysqlEscape ( T ) ( T input ) @("mysqlEscape") debug(MYSQLN_TESTS) -unittest +@safe unittest { import std.array : appender; diff --git a/source/mysql/internal/connection.d b/source/mysql/impl/connection.d similarity index 98% rename from source/mysql/internal/connection.d rename to source/mysql/impl/connection.d index 06ebdd6a..a14f06f7 100644 --- a/source/mysql/internal/connection.d +++ b/source/mysql/impl/connection.d @@ -1,5 +1,15 @@ -/// Connect to a MySQL/MariaDB server. -module mysql.internal.connection; +/++ +Connect to a MySQL/MariaDB server. + +WARNING: +This module is used to consolidate the common implementation of the safe and +unafe API. DO NOT directly import this module, please import one of +`mysql.connection`, `mysql.safe.connection`, or `mysql.unsafe.connection`. This +module will be removed in a future version without deprecation. + +$(SAFE_MIGRATION) ++/ +module mysql.impl.connection; import std.algorithm; import std.conv; @@ -14,8 +24,8 @@ import mysql.protocol.comms; import mysql.protocol.constants; import mysql.protocol.packets; import mysql.protocol.sockets; -import mysql.internal.result; -import mysql.internal.prepared; +import mysql.impl.result; +import mysql.impl.prepared; import mysql.types; @safe: diff --git a/source/mysql/internal/pool.d b/source/mysql/impl/pool.d similarity index 94% rename from source/mysql/internal/pool.d rename to source/mysql/impl/pool.d index 8171f40e..5ddd125b 100644 --- a/source/mysql/internal/pool.d +++ b/source/mysql/impl/pool.d @@ -8,13 +8,21 @@ If you don't want to, refer to `mysql.connection.Connection`. This provides various benefits over creating a new connection manually, such as automatically reusing old connections, and automatic cleanup (no need to close the connection when done). + +WARNING: +This module is used to consolidate the common implementation of the safe and +unafe API. DO NOT directly import this module, please import one of +`mysql.pool`, `mysql.safe.pool`, or `mysql.unsafe.pool`. This module will be +removed in a future version without deprecation. + +$(SAFE_MIGRATION) +/ -module mysql.internal.pool; +module mysql.impl.pool; import std.conv; import std.typecons; -import mysql.connection; -import mysql.prepared; +import mysql.impl.connection; +import mysql.impl.prepared; import mysql.protocol.constants; debug(MYSQLN_TESTS) { @@ -86,8 +94,14 @@ version(IncludeMySQLPool) If, for any reason, this class doesn't suit your needs, it's easy to just use vibe.d's $(LINK2 http://vibed.org/api/vibe.core.connectionpool/ConnectionPool, ConnectionPool) - directly. Simply provide it with a delegate that creates a new `mysql.connection.Connection` - and does any other custom processing if needed. + directly. Simply provide it with a delegate that creates a new + `mysql.impl.connection.Connection` and does any other custom processing if + needed. + + You should not use this template directly, but rather import + `mysql.safe.pool` or `mysql.unsafe.pool` or `mysql.pool`, which will alias + MySQLPool to the correct instantiation. The boolean parameter here + specifies whether the pool is operating in safe mode or unsafe mode. +/ class MySQLPoolImpl(bool isSafe) { @@ -198,7 +212,7 @@ version(IncludeMySQLPool) return lockConnectionImpl(); } - // the implementation we want to imply attributes + // the implementation we want to infer attributes private final lockConnectionImpl() { auto conn = m_pool.lockConnection(); @@ -376,7 +390,7 @@ version(IncludeMySQLPool) /// Is the given statement set to be automatically registered on all /// connections obtained from this connection pool? - bool isAutoRegistered(Prepared prepared) @safe + bool isAutoRegistered(SafePrepared prepared) @safe { return isAutoRegistered(prepared.sql); } @@ -393,7 +407,7 @@ version(IncludeMySQLPool) /// Is the given statement set to be automatically released on all /// connections obtained from this connection pool? - bool isAutoReleased(Prepared prepared) @safe + bool isAutoReleased(SafePrepared prepared) @safe { return isAutoReleased(prepared.sql); } @@ -415,7 +429,7 @@ version(IncludeMySQLPool) Equivalent to `!isAutoRegistered && !isAutoReleased`. +/ - bool isAutoCleared(Prepared prepared) @safe + bool isAutoCleared(SafePrepared prepared) @safe { return isAutoCleared(prepared.sql); } @@ -490,9 +504,15 @@ version(IncludeMySQLPool) static void doit(bool isSafe)() { static if(isSafe) + { import mysql.safe.commands; + import mysql.safe.connection; + } else + { import mysql.unsafe.commands; + import mysql.unsafe.connection; + } alias MySQLPool = MySQLPoolImpl!isSafe; auto pool = new MySQLPool(testConnectionStr); diff --git a/source/mysql/internal/prepared.d b/source/mysql/impl/prepared.d similarity index 89% rename from source/mysql/internal/prepared.d rename to source/mysql/impl/prepared.d index 867526be..e0204f56 100644 --- a/source/mysql/internal/prepared.d +++ b/source/mysql/impl/prepared.d @@ -1,5 +1,15 @@ -/// Use a DB via SQL prepared statements. -module mysql.internal.prepared; +/++ +Use a DB via SQL prepared statements. + +WARNING: +This module is used to consolidate the common implementation of the safe and +unafe API. DO NOT directly import this module, please import one of +`mysql.prepared`, `mysql.safe.prepared`, or `mysql.unsafe.prepared`. This +module will be removed in a future version without deprecation. + +$(SAFE_MIGRATION) ++/ +module mysql.impl.prepared; import std.exception; import std.range; @@ -12,7 +22,7 @@ import mysql.protocol.comms; import mysql.protocol.constants; import mysql.protocol.packets; import mysql.types; -import mysql.internal.result; +import mysql.impl.result; import mysql.safe.commands : ColumnSpecialization, CSN; debug(MYSQLN_TESTS) import mysql.test.common; @@ -26,6 +36,8 @@ If both are provided then the corresponding column will be populated by calling The source should fill the indicated slice with data and arrange for the delegate to return the length of the data supplied (in bytes). If that is less than the `chunkSize` then the chunk will be assumed to be the last one. + +Please use one of the aliases instead of the Impl struct, as this name likely will be removed without deprecation in a future release. +/ struct ParameterSpecializationImpl(bool isSafe) { @@ -148,15 +160,15 @@ unittest /++ Encapsulation of a prepared statement. -Create this via the function `mysql.connection.prepare`. Set your arguments (if any) via +Create this via the function `mysql.safe.connection.prepare`. Set your arguments (if any) via the functions provided, and then run the statement by passing it to -`mysql.commands.exec`/`mysql.commands.query`/etc in place of the sql string parameter. +`mysql.safe.commands.exec`/`mysql.safe.commands.query`/etc in place of the sql string parameter. Commands that are expected to return a result set - queries - have distinctive methods that are enforced. That is it will be an error to call such a method with an SQL command that does not produce a result set. So for commands like -SELECT, use the `mysql.commands.query` functions. For other commands, like -INSERT/UPDATE/CREATE/etc, use `mysql.commands.exec`. +SELECT, use the `mysql.safe.commands.query` functions. For other commands, like +INSERT/UPDATE/CREATE/etc, use `mysql.safe.commands.exec`. +/ struct SafePrepared { @@ -179,12 +191,12 @@ package(mysql): public: /++ - Constructor. You probably want `mysql.connection.prepare` instead of this. + Constructor. You probably want `mysql.safe.connection.prepare` instead of this. - Call `mysqln.connection.prepare` instead of this, unless you are creating - your own transport bypassing `mysql.connection.Connection` entirely. + Call `mysqln.safe.connection.prepare` instead of this, unless you are creating + your own transport bypassing `mysql.impl.connection.Connection` entirely. The prepared statement must be registered on the server BEFORE this is - called (which `mysqln.connection.prepare` does). + called (which `mysqln.safe.connection.prepare` does). Internally, the result of a successful outcome will be a statement handle - an ID - for the prepared statement, a count of the parameters required for @@ -368,10 +380,7 @@ public: Type_Mappings: $(TYPE_MAPPINGS) Params: index = The zero based index - - Note: The type of getArg's return is now MySQLVal. As a stop-gap measure, - mysql-native provides the vGetArg version. This version will be removed - in a future update. + Returns: The MySQLVal representing the argument. +/ MySQLVal getArg(size_t index) { @@ -379,13 +388,6 @@ public: return _inParams[index]; } - /// ditto - Variant vGetArg(size_t index) @system - { - // convert to Variant. - return getArg(index).asVariant; - } - /++ Sets a prepared statement parameter to NULL. @@ -396,6 +398,7 @@ public: Params: index = The zero based index +/ + deprecated("Please use setArg(index, null)") void setNullArg(size_t index) { setArg(index, null); @@ -466,7 +469,7 @@ public: assert(rs[1].isNull(0)); assert(rs[2].isNull(0)); assert(rs[1][0].kind == MySQLVal.Kind.Null); - assert(rs[2][0].kind == MySQLVal.Kind.Null); + assert(rs[2][0] == null); } /// Gets the number of arguments this prepared statement expects to be passed in. @@ -515,10 +518,23 @@ public: @property void columnSpecials(ColumnSpecialization[] csa) pure { _columnSpecials = csa; } } -// unsafe wrapper +/++ +Unsafe wrapper for SafePrepared. + +This wrapper contains a SafePrepared, and forwards common functionality to that +type. It overrides the setting and fetching of arguments, converting them to +and from Variant for backwards compatibility. + +It also sets up UnsafeParameterSpecialization items for the parameters. Note +that these are simply cast to SafeParameterSpecialization. There are runtime +guards in place to ensure a SafeParameterSpecialization with an unsafe delegate +is not accessible as a safe delegate. + +$(SAFE_MIGRATION) ++/ struct UnsafePrepared { - SafePrepared _safe; + private SafePrepared _safe; private this(SafePrepared sp) @safe { @@ -530,25 +546,31 @@ struct UnsafePrepared _safe = SafePrepared(sql, headers, numParams); } - // redefine all functions that deal with unsafe types + /++ + Redefine all functions that deal with MySQLVal to deal with Variant instead. Please see SafePrepared for details on how the methods work. + + $(SAFE_MIGRATION) + +/ void setArg(T)(size_t index, T val, UnsafeParameterSpecialization psn = UPSN.init) @system if(!is(T == Variant)) { - _safe.setArg(index, val, cast(SafeParameterSpecialization)psn); + _safe.setArg(index, val, cast(SPSN)psn); } + /// ditto void setArg(size_t index, Variant val, UnsafeParameterSpecialization psn = UPSN.init) @system { _safe.setArg(index, _toVal(val), cast(SPSN)psn); } - // unfortunately, we need to redefine this here + /// ditto void setArgs(T...)(T args) if(T.length == 0 || (!is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]))) { _safe.setArgs(args); } + /// ditto void setArgs(Variant[] args, UnsafeParameterSpecialization[] psnList=null) @system { enforce!MYX(args.length == _numParams, "Param count supplied does not match prepared statement"); @@ -561,26 +583,36 @@ struct UnsafePrepared } } + /// ditto Variant getArg(size_t index) @system { return _safe.getArg(index).asVariant; } - alias _safe this; + /++ + Allow conversion to a SafePrepared. UnsafePrepared with + UnsafeParameterSpecialization items that have chunk delegates are not + allowed to convert, because the delegates are possibly unsafe. + +/ + ref SafePrepared safe() scope return @safe + { + // first, ensure there are no parameter specializations with delegates as + // those are possibly unsafe. + foreach(ref s; _safe._psa) + enforce!MYX(s.chunkDelegate is null, "Cannot convert UnsafePrepared into SafePrepared with unsafe chunk delegates"); + return _safe; + } + + /// Forward all other calls to the safe accessor + alias safe this; } +/// Allow conversion to UnsafePrepared from SafePrepared. UnsafePrepared unsafe(SafePrepared p) @safe { return UnsafePrepared(p); } -SafePrepared safe(UnsafePrepared p) @safe -{ - return p._safe; -} - - - /// Template constraint for `PreparedRegistrations` private enum isPreparedRegistrationsPayload(Payload) = __traits(compiles, (){ @@ -593,7 +625,7 @@ private enum isPreparedRegistrationsPayload(Payload) = Common functionality for recordkeeping of prepared statement registration and queueing for unregister. -Used by `Connection` and `MySQLPool`. +Used by `Connection` and `MySQLPoolImpl`. Templated on payload type. The payload should be an aggregate that includes the field: `bool queuedForRelease = false;` diff --git a/source/mysql/internal/result.d b/source/mysql/impl/result.d similarity index 74% rename from source/mysql/internal/result.d rename to source/mysql/impl/result.d index 39a20aea..3d24de40 100644 --- a/source/mysql/internal/result.d +++ b/source/mysql/impl/result.d @@ -1,12 +1,21 @@ -/// Structures for data received: rows and result sets (ie, a range of rows). -module mysql.internal.result; +/++ +Structures for data received: rows and result sets (ie, a range of rows). + +WARNING: +This module is used to consolidate the common implementation of the safe and +unafe API. DO NOT directly import this module, please import one of +`mysql.result`, `mysql.safe.result`, or `mysql.unsafe.result`. This module will +be removed in a future version without deprecation. + +$(SAFE_MIGRATION) ++/ +module mysql.impl.result; import std.conv; import std.exception; import std.range; import std.string; -import mysql.connection; import mysql.exceptions; import mysql.protocol.comms; import mysql.protocol.extra_types; @@ -22,20 +31,20 @@ Type_Mappings: $(TYPE_MAPPINGS) +/ /+ The row struct is used for both 'traditional' and 'prepared' result sets. -It consists of parallel arrays of Variant and bool, with the bool array +It consists of parallel arrays of MySQLVal and bool, with the bool array indicating which of the result set columns are NULL. I have been agitating for some kind of null indicator that can be set for a -Variant without destroying its inherent type information. If this were the +MySQLVal without destroying its inherent type information. If this were the case, then the bool array could disappear. +/ struct SafeRow { - import mysql.connection; package(mysql): MySQLVal[] _values; // Temporarily "package" instead of "private" private: + import mysql.impl.connection; bool[] _nulls; string[] _names; @@ -61,13 +70,15 @@ public: /++ Simplify retrieval of a column value by index. - To check for null, use Variant's `type` property: - `row[index].type == typeid(typeof(null))` + To check for null, use MySQLVal's `kind` property: + `row[index].kind == MySQLVal.Kind.Null` + or use a direct comparison to null: + `row[index] == null` Type_Mappings: $(TYPE_MAPPINGS) Params: i = the zero based index of the column whose value is required. - Returns: A Variant holding the column value. + Returns: A MySQLVal holding the column value. +/ ref inout(MySQLVal) opIndex(size_t i) inout { @@ -128,9 +139,9 @@ public: Move the content of the row into a compatible struct This method takes no account of NULL column values. If a column was NULL, - the corresponding Variant value would be unchanged in those cases. + the corresponding MySQLVal value would be unchanged in those cases. - The method will throw if the type of the Variant is not implicitly + The method will throw if the type of the MySQLVal is not implicitly convertible to the corresponding struct member. Type_Mappings: $(TYPE_MAPPINGS) @@ -177,11 +188,24 @@ public: } } -/// ditto +/+ +An UnsafeRow is almost identical to a SafeRow, except that it provides access +to its values via Variant instead of MySQLVal. This makes the access unsafe. +Only value access is unsafe, every other operation is forwarded to the internal +SafeRow. + +Use the safe or unsafe UFCS methods to convert to and from these two types if +needed. + +Note that there is a performance penalty when accessing via a Variant as the MySQLVal must be converted on every access. + +$(SAFE_MIGRATION) ++/ struct UnsafeRow { SafeRow _safe; alias _safe this; + /// Converts SafeRow.opIndex result to Variant. Variant opIndex(size_t idx) { return _safe[idx].asVariant; } @@ -202,12 +226,14 @@ Nullable!UnsafeRow unsafe(Nullable!SafeRow r) @safe } +/// ditto SafeRow safe(UnsafeRow r) @safe { return r._safe; } +/// ditto Nullable!SafeRow safe(Nullable!UnsafeRow r) @safe { if(r.isNull) @@ -217,37 +243,39 @@ Nullable!SafeRow safe(Nullable!UnsafeRow r) @safe /++ An $(LINK2 http://dlang.org/phobos/std_range_primitives.html#isInputRange, input range) -of Row. +of SafeRow. -This is returned by the `mysql.commands.query` functions. +This is returned by the `mysql.safe.commands.query` functions. The rows are downloaded one-at-a-time, as you iterate the range. This allows for low memory usage, and quick access to the results as they are downloaded. This is especially ideal in case your query results in a large number of rows. -However, because of that, this `ResultRange` cannot offer random access or +However, because of that, this `SafeResultRange` cannot offer random access or a `length` member. If you need random access, then just like any other range, you can simply convert this range to an array via $(LINK2 https://dlang.org/phobos/std_array.html#array, `std.array.array()`). -A `ResultRange` becomes invalidated (and thus cannot be used) when the server -is sent another command on the same connection. When an invalidated `ResultRange` -is used, a `mysql.exceptions.MYXInvalidatedRange` is thrown. If you need to -send the server another command, but still access these results afterwords, -you can save the results for later by converting this range to an array via +A `SafeResultRange` becomes invalidated (and thus cannot be used) when the server +is sent another command on the same connection. When an invalidated +`SafeResultRange` is used, a `mysql.exceptions.MYXInvalidatedRange` is thrown. +If you need to send the server another command, but still access these results +afterwords, you can save the results for later by converting this range to an +array via $(LINK2 https://dlang.org/phobos/std_array.html#array, `std.array.array()`). Type_Mappings: $(TYPE_MAPPINGS) Example: --- -ResultRange oneAtATime = myConnection.query("SELECT * from myTable"); -Row[] allAtOnce = myConnection.query("SELECT * from myTable").array; +SafeResultRange oneAtATime = myConnection.query("SELECT * from myTable"); +SafeRow[] allAtOnce = myConnection.query("SELECT * from myTable").array; --- +/ struct SafeResultRange { private: + import mysql.impl.connection; @safe: Connection _con; ResultSetHeaders _rsh; @@ -277,11 +305,12 @@ public: /++ Check whether the range can still be used, or has been invalidated. - A `ResultRange` becomes invalidated (and thus cannot be used) when the server - is sent another command on the same connection. When an invalidated `ResultRange` - is used, a `mysql.exceptions.MYXInvalidatedRange` is thrown. If you need to - send the server another command, but still access these results afterwords, - you can save the results for later by converting this range to an array via + A `SafeResultRange` becomes invalidated (and thus cannot be used) when the + server is sent another command on the same connection. When an invalidated + `SafeResultRange` is used, a `mysql.exceptions.MYXInvalidatedRange` is + thrown. If you need to send the server another command, but still access + these results afterwords, you can save the results for later by converting + this range to an array via $(LINK2 https://dlang.org/phobos/std_array.html#array, `std.array.array()`). +/ @property bool isValid() const pure nothrow @@ -366,13 +395,23 @@ public: @property ulong rowCount() const pure nothrow { return _numRowsFetched; } } -/// ditto +/+ +A wrapper of a SafeResultRange which converts each row into an UnsafeRow. + +Use the safe or unsafe UFCS methods to convert to and from these two types if +needed. + +$(SAFE_MIGRATION) ++/ struct UnsafeResultRange { + /// The underlying range is a SafeResultRange. SafeResultRange safe; alias safe this; + /// Equivalent to SafeResultRange.front, but wraps as an UnsafeRow. inout(UnsafeRow) front() inout { return inout(UnsafeRow)(safe.front); } + /// Equivalent to SafeResultRange.asAA, but converts each value to a Variant Variant[string] asAA() { ensureValid(); @@ -384,7 +423,7 @@ struct UnsafeResultRange } } -/// ditto +/// Wrap a SafeResultRange as an UnsafeResultRange. UnsafeResultRange unsafe(SafeResultRange r) @safe { return UnsafeResultRange(r); diff --git a/source/mysql/package.d b/source/mysql/package.d index 8c3ea75d..a981570d 100644 --- a/source/mysql/package.d +++ b/source/mysql/package.d @@ -59,19 +59,20 @@ $(TABLE $(TR $(TD string ) $(TD VARCHAR )) $(TR $(TD char[] ) $(TD VARCHAR )) $(TR $(TD (u)byte[] ) $(TD SIGNED TINYBLOB )) - $(TR $(TD other ) $(TD unsupported (throws) )) + $(TR $(TD other ) $(TD unsupported with Variant (throws) or MySQLVal (compiler error) )) ) Note: This by default imports the unsafe version of the MySQL API. Please switch to the safe version (`import mysql.safe`) as this will be the default in the future. If you would prefer to use the unsafe version, it is advised to use the import `mysql.unsafe`, as this will be supported for at least one more -major version. +major version, albeit deprecated. + +$(SAFE_MIGRATION) +/ module mysql; -// by default we do the unsafe API. This will change in a future version to the -// safe one. +// by default we do the unsafe API. public import mysql.unsafe; debug(MYSQLN_TESTS) version = DoCoreTests; diff --git a/source/mysql/pool.d b/source/mysql/pool.d index c9b341d9..1a879673 100644 --- a/source/mysql/pool.d +++ b/source/mysql/pool.d @@ -1,3 +1,16 @@ +/++ +This module publicly imports `mysql.unsafe.pool`, which provides backwards +compatible functions for using vibe.d's +$(LINK2 http://vibed.org/api/vibe.core.connectionpool/ConnectionPool, ConnectionPool). + +Please see the module documentation in `mysql.impl.pool` for more details. + +In the future, this will migrate to importing `mysql.safe.pool`. In the far +future, the unsafe version will be deprecated and removed, and the safe version +moved to this location. + +$(SAFE_MIGRATION) ++/ module mysql.pool; public import mysql.unsafe.pool; diff --git a/source/mysql/prepared.d b/source/mysql/prepared.d index 920d016d..7a7b4f14 100644 --- a/source/mysql/prepared.d +++ b/source/mysql/prepared.d @@ -1,3 +1,12 @@ +/++ +This module publicly imports `mysql.unsafe.prepared`. Please see that module for more documentation. + +In the future, this will migrate to importing `mysql.safe.prepared`. In the +far future, the unsafe version will be deprecated and removed, and the safe +version moved to this location. + +$(SAFE_MIGRATION) ++/ module mysql.prepared; public import mysql.unsafe.prepared; diff --git a/source/mysql/result.d b/source/mysql/result.d index 942d613f..e793a070 100644 --- a/source/mysql/result.d +++ b/source/mysql/result.d @@ -1,3 +1,19 @@ +/++ +This module publicly imports `mysql.unsafe.result`, which provides backwards +compatible structures for processing rows of data from a MySQL server. Please +see that module for details on usage. + +It is recommended instead ot import `mysql.safe.result`, which provides +@safe-only mechanisms for processing rows of data. + +Note that the actual structs are documented in `mysql.impl.result`. + +In the future, this will migrate to importing `mysql.safe.result`. In the far +future, the unsafe version will be deprecated and removed, and the safe version +moved to this location. + +$(SAFE_MIGRATION) +++/ module mysql.result; public import mysql.unsafe.result; diff --git a/source/mysql/safe/commands.d b/source/mysql/safe/commands.d index da439b1b..a71f84bb 100644 --- a/source/mysql/safe/commands.d +++ b/source/mysql/safe/commands.d @@ -1,11 +1,15 @@ /++ -Use a DB via plain SQL statements. - Commands that are expected to return a result set - queries - have distinctive methods that are enforced. That is it will be an error to call such a method with an SQL command that does not produce a result set. So for commands like SELECT, use the `query` functions. For other commands, like INSERT/UPDATE/CREATE/etc, use `exec`. + +This is the @safe version of mysql's command module, and as such uses the @safe +rows and result ranges, and the `MySQLVal` type. For the `Variant` unsafe +version, please import `mysql.unsafe.commands`. + +$(SAFE_MIGRATION) +/ module mysql.safe.commands; @@ -23,7 +27,7 @@ import mysql.protocol.comms; import mysql.protocol.constants; import mysql.protocol.extra_types; import mysql.protocol.packets; -import mysql.internal.result; +import mysql.impl.result; import mysql.types; /// This feature is not yet implemented. It currently has no effect. @@ -167,25 +171,27 @@ same statement instead of manually preparing a statement. If `args` and `prepared` are both provided, `args` will be used, and any arguments that are already set in the prepared statement will automatically be replaced with `args` (note, just like calling -`mysql.prepared.Prepared.setArgs`, this will also remove all -`mysql.prepared.ParameterSpecialization` that may have been applied). +`mysql.impl.prepared.SafePrepared.setArgs`, this will also remove all +`mysql.impl.prepared.SafeParameterSpecialization` that may have been applied). Only use the `const(char[]) sql` overload that doesn't take `args` when you are not going to be using the same command repeatedly and you are CERTAIN all the data you're sending is properly escaped. Otherwise, consider using overload that takes a `Prepared`. -If you need to use any `mysql.prepared.ParameterSpecialization`, use -`mysql.connection.prepare` to manually create a `mysql.prepared.Prepared`, -and set your parameter specializations using `mysql.prepared.Prepared.setArg` -or `mysql.prepared.Prepared.setArgs`. +If you need to use any `mysql.impl.prepared.SafeParameterSpecialization`, use +`mysql.safe.connection.prepare` to manually create a +`mysql.impl.prepared.SafePrepared`, and set your parameter specializations using +`mysql.impl.prepared.SafePrepared.setArg` or +`mysql.impl.prepared.SafePrepared.setArgs`. Type_Mappings: $(TYPE_MAPPINGS) Params: -conn = An open `mysql.connection.Connection` to the database. +conn = An open `mysql.impl.connection.Connection` to the database. sql = The SQL command to be run. prepared = The prepared statement to be run. +args = The arguments to be passed in the `mysql.impl.prepared.SafePrepared`. Returns: The number of rows affected. @@ -255,8 +261,8 @@ package ulong execImpl(Connection conn, ExecQueryImplInfo info) /++ Execute an SQL SELECT command or prepared statement. -This returns an input range of `mysql.result.Row`, so if you need random access -to the `mysql.result.Row` elements, simply call +This returns an input range of `mysql.impl.result.SafeRow`, so if you need random +access to the `mysql.impl.result.SafeRow` elements, simply call $(LINK2 https://dlang.org/phobos/std_array.html#array, `std.array.array()`) on the result. @@ -272,28 +278,30 @@ same statement instead of manually preparing a statement. If `args` and `prepared` are both provided, `args` will be used, and any arguments that are already set in the prepared statement will automatically be replaced with `args` (note, just like calling -`mysql.prepared.Prepared.setArgs`, this will also remove all -`mysql.prepared.ParameterSpecialization` that may have been applied). +`mysql.impl.prepared.SafePrepared.setArgs`, this will also remove all +`mysql.impl.prepared.SafeParameterSpecialization` that may have been applied). Only use the `const(char[]) sql` overload that doesn't take `args` when you are not going to be using the same command repeatedly and you are CERTAIN all the data you're sending is properly escaped. Otherwise, consider using overload that takes a `Prepared`. -If you need to use any `mysql.prepared.ParameterSpecialization`, use -`mysql.connection.prepare` to manually create a `mysql.prepared.Prepared`, -and set your parameter specializations using `mysql.prepared.Prepared.setArg` -or `mysql.prepared.Prepared.setArgs`. +If you need to use any `mysql.safe.prepared.ParameterSpecialization`, use +`mysql.safe.connection.prepare` to manually create a +`mysql.impl.prepared.SafePrepared`, and set your parameter specializations using +`mysql.impl.prepared.SafePrepared.setArg` or +`mysql.impl.prepared.SafePrepared.setArgs`. Type_Mappings: $(TYPE_MAPPINGS) Params: -conn = An open `mysql.connection.Connection` to the database. +conn = An open `mysql.impl.connection.Connection` to the database. sql = The SQL command to be run. prepared = The prepared statement to be run. csa = Not yet implemented. +args = Arguments to the SQL statement or `mysql.safe.prepared.Prepared` struct. -Returns: A (possibly empty) `mysql.result.ResultRange`. +Returns: A (possibly empty) `mysql.safe.result.ResultRange`. Example: --- @@ -373,7 +381,7 @@ package SafeResultRange queryImpl(ColumnSpecialization[] csa, /++ Execute an SQL SELECT command or prepared statement where you only want the -first `mysql.result.Row`, if any. +first `mysql.impl.result.SafeRow`, if any. If the SQL command does not produce a result set (such as INSERT/CREATE/etc), then `mysql.exceptions.MYXNoResultRecieved` will be thrown. Use @@ -387,29 +395,31 @@ same statement instead of manually preparing a statement. If `args` and `prepared` are both provided, `args` will be used, and any arguments that are already set in the prepared statement will automatically be replaced with `args` (note, just like calling -`mysql.prepared.Prepared.setArgs`, this will also remove all -`mysql.prepared.ParameterSpecialization` that may have been applied). +`mysql.impl.prepared.SafePrepared.setArgs`, this will also remove all +`mysql.impl.prepared.SafeParameterSpecialization` that may have been applied). Only use the `const(char[]) sql` overload that doesn't take `args` when you are not going to be using the same command repeatedly and you are CERTAIN all the data you're sending is properly escaped. Otherwise, consider using overload that takes a `Prepared`. -If you need to use any `mysql.prepared.ParameterSpecialization`, use -`mysql.connection.prepare` to manually create a `mysql.prepared.Prepared`, -and set your parameter specializations using `mysql.prepared.Prepared.setArg` -or `mysql.prepared.Prepared.setArgs`. +If you need to use any `mysql.impl.prepared.SafeParameterSpecialization`, use +`mysql.safe.connection.prepare` to manually create a +`mysql.impl.prepared.SafePrepared`, and set your parameter specializations using +`mysql.impl.prepared.SafePrepared.setArg` or +`mysql.impl.prepared.SafePrepared.setArgs`. Type_Mappings: $(TYPE_MAPPINGS) Params: -conn = An open `mysql.connection.Connection` to the database. +conn = An open `mysql.impl.connection.Connection` to the database. sql = The SQL command to be run. prepared = The prepared statement to be run. csa = Not yet implemented. +args = Arguments to SQL statement or `mysql.impl.prepared.SafePrepared` struct. -Returns: `Nullable!(mysql.result.Row)`: This will be null (check via `Nullable.isNull`) if the -query resulted in an empty result set. +Returns: `Nullable!(mysql.impl.result.SafeRow)`: This will be null (check + via `Nullable.isNull`) if the query resulted in an empty result set. Example: --- @@ -513,7 +523,7 @@ escaped. Otherwise, consider using overload that takes a `Prepared`. Type_Mappings: $(TYPE_MAPPINGS) Params: -conn = An open `mysql.connection.Connection` to the database. +conn = An open `mysql.impl.connection.Connection` to the database. sql = The SQL command to be run. prepared = The prepared statement to be run. args = The variables, taken by reference, to receive the values. @@ -578,11 +588,13 @@ Execute an SQL SELECT command or prepared statement and return a single value: the first column of the first row received. If the query did not produce any rows, or the rows it produced have zero columns, -this will return `Nullable!Variant()`, ie, null. Test for this with `result.isNull`. +this will return `Nullable!MySQLVal()`, ie, null. Test for this with +`result.isNull`. If the query DID produce a result, but the value actually received is NULL, -then `result.isNull` will be FALSE, and `result.get` will produce a Variant -which CONTAINS null. Check for this with `result.get.type == typeid(typeof(null))`. +then `result.isNull` will be FALSE, and `result.get` will produce a MySQLVal +which CONTAINS null. Check for this with `result.get.kind == MySQLVal.Kind.Null` +or `result.get == null`. If the SQL command does not produce a result set (such as INSERT/CREATE/etc), then `mysql.exceptions.MYXNoResultRecieved` will be thrown. Use @@ -596,34 +608,34 @@ same statement instead of manually preparing a statement. If `args` and `prepared` are both provided, `args` will be used, and any arguments that are already set in the prepared statement will automatically be replaced with `args` (note, just like calling -`mysql.prepared.Prepared.setArgs`, this will also remove all -`mysql.prepared.ParameterSpecialization` that may have been applied). +`mysql.impl.prepared.SafePrepared.setArgs`, this will also remove all +`mysql.impl.prepared.SafeParameterSpecialization` that may have been applied). Only use the `const(char[]) sql` overload that doesn't take `args` when you are not going to be using the same command repeatedly and you are CERTAIN all the data you're sending is properly escaped. Otherwise, consider using overload that takes a `Prepared`. -If you need to use any `mysql.prepared.ParameterSpecialization`, use -`mysql.connection.prepare` to manually create a `mysql.prepared.Prepared`, -and set your parameter specializations using `mysql.prepared.Prepared.setArg` -or `mysql.prepared.Prepared.setArgs`. +If you need to use any `mysql.impl.prepared.SafeParameterSpecialization`, use +`mysql.safe.connection.prepare` to manually create a `mysql.impl.prepared.SafePrepared`, +and set your parameter specializations using `mysql.impl.prepared.SafePrepared.setArg` +or `mysql.impl.prepared.SafePrepared.setArgs`. Type_Mappings: $(TYPE_MAPPINGS) Params: -conn = An open `mysql.connection.Connection` to the database. +conn = An open `mysql.impl.connection.Connection` to the database. sql = The SQL command to be run. prepared = The prepared statement to be run. csa = Not yet implemented. -Returns: `Nullable!Variant`: This will be null (check via `Nullable.isNull`) if the +Returns: `Nullable!MySQLVal`: This will be null (check via `Nullable.isNull`) if the query resulted in an empty result set. Example: --- auto myInt = 7; -Nullable!Variant value = myConnection.queryRow("SELECT * FROM `myTable` WHERE `a` = ?", myInt); +Nullable!MySQLVal value = myConnection.queryRow("SELECT * FROM `myTable` WHERE `a` = ?", myInt); --- +/ /+ diff --git a/source/mysql/safe/connection.d b/source/mysql/safe/connection.d index db50bd42..df7b2585 100644 --- a/source/mysql/safe/connection.d +++ b/source/mysql/safe/connection.d @@ -1,6 +1,13 @@ +/++ +Connect to a MySQL/MariaDB server. + +This is the @safe API for the Connection type. It publicly imports `mysql.impl.connection`, and also provides the safe version of the API for preparing statements. + +$(SAFE_MIGRATION) ++/ module mysql.safe.connection; -public import mysql.internal.connection; +public import mysql.impl.connection; import mysql.safe.prepared; import mysql.safe.commands; @@ -11,7 +18,7 @@ import mysql.safe.commands; Submit an SQL command to the server to be compiled into a prepared statement. This will automatically register the prepared statement on the provided connection. -The resulting `mysql.prepared.Prepared` can then be used freely on ANY `Connection`, +The resulting `mysql.impl.prepared.SafePrepared` can then be used freely on ANY `Connection`, as it will automatically be registered upon its first use on other connections. Or, pass it to `Connection.register` if you prefer eager registration. @@ -26,10 +33,10 @@ followed by an EOF packet. Throws: `mysql.exceptions.MYX` if the server has a problem. +/ -Prepared prepare(Connection conn, const(char[]) sql) +SafePrepared prepare(Connection conn, const(char[]) sql) { auto info = conn.registerIfNeeded(sql); - return Prepared(sql, info.headers, info.numParams); + return SafePrepared(sql, info.headers, info.numParams); } /++ @@ -44,7 +51,7 @@ Params: name = The name of the stored function. numArgs = The number of arguments the stored procedure takes. +/ -Prepared prepareFunction(Connection conn, string name, int numArgs) +SafePrepared prepareFunction(Connection conn, string name, int numArgs) { auto sql = "select " ~ name ~ preparedPlaceholderArgs(numArgs); return prepare(conn, sql); @@ -89,7 +96,7 @@ Params: numArgs = The number of arguments the stored procedure takes. +/ -Prepared prepareProcedure(Connection conn, string name, int numArgs) +SafePrepared prepareProcedure(Connection conn, string name, int numArgs) { auto sql = "call " ~ name ~ preparedPlaceholderArgs(numArgs); return prepare(conn, sql); diff --git a/source/mysql/safe/package.d b/source/mysql/safe/package.d index 2bb1071c..ddc331c9 100644 --- a/source/mysql/safe/package.d +++ b/source/mysql/safe/package.d @@ -3,6 +3,8 @@ Imports all of $(LINK2 https://github.com/mysql-d/mysql-native, mysql-native). This module will import all modules that use the safe API of the mysql library. In a future version, this will become the default. + +$(SAFE_MIGRATION) +/ module mysql.safe; @@ -10,9 +12,9 @@ public import mysql.safe.commands; public import mysql.safe.result; public import mysql.safe.pool; public import mysql.safe.prepared; +public import mysql.safe.connection; // common imports -public import mysql.connection; public import mysql.escape; public import mysql.exceptions; public import mysql.metadata; diff --git a/source/mysql/safe/pool.d b/source/mysql/safe/pool.d index 69df382a..7455ccd4 100644 --- a/source/mysql/safe/pool.d +++ b/source/mysql/safe/pool.d @@ -1,6 +1,20 @@ +/++ +Connect to a MySQL/MariaDB database using vibe.d's +$(LINK2 http://vibed.org/api/vibe.core.connectionpool/ConnectionPool, ConnectionPool). + +This aliases `mysql.impl.pool.MySQLPoolImpl!true` as MySQLPool. Please see the +`mysql.impl.pool` moddule for documentation on how to use MySQLPool. + +This is the @safe version of mysql's pool module, and as such uses only @safe +callback delegates. If you wish to use @system callbacks, import +`mysql.unsafe.pool`. + +$(SAFE_MIGRATION) ++/ + module mysql.safe.pool; -import mysql.internal.pool; +import mysql.impl.pool; // need to check if mysqlpool was enabled static if(__traits(compiles, () { alias p = MySQLPoolImpl!true; })) alias MySQLPool = MySQLPoolImpl!true; diff --git a/source/mysql/safe/prepared.d b/source/mysql/safe/prepared.d index 2c3309e9..cc860a23 100644 --- a/source/mysql/safe/prepared.d +++ b/source/mysql/safe/prepared.d @@ -1,6 +1,22 @@ +/++ +This module publicly imports `mysql.impl.prepared`. See that module for +documentation on using prepared statements with a MySQL server. + +This module also aliases the safe versions of structs to the original struct +names to aid in transitioning to using safe code with minimal impact. + +$(SAFE_MIGRATION) ++/ module mysql.safe.prepared; -public import mysql.internal.prepared; +public import mysql.impl.prepared; + +/++ +Safe aliases. Use these instead of the real name. See the documentation on +the aliased types for usage. ++/ alias Prepared = SafePrepared; +/// ditto alias ParameterSpecialization = SafeParameterSpecialization; +/// ditto alias PSN = SafeParameterSpecialization; diff --git a/source/mysql/safe/result.d b/source/mysql/safe/result.d index 3a26d73c..d416d122 100644 --- a/source/mysql/safe/result.d +++ b/source/mysql/safe/result.d @@ -1,6 +1,19 @@ +/++ +This module publicly imports `mysql.impl.result`. See that module for documentation on how to use result and result range structures. + +This module also aliases the safe versions of these structs to the original +struct names to aid in transitioning to using safe code with minimal impact. + +$(SAFE_MIGRATION) ++/ module mysql.safe.result; -public import mysql.internal.result; +public import mysql.impl.result; +/++ +Safe aliases. Use these instead of the real name. See the documentation on +the aliased types for usage. ++/ alias Row = SafeRow; +/// ditto alias ResultRange = SafeResultRange; diff --git a/source/mysql/types.d b/source/mysql/types.d index bfc1dacb..6084e1c5 100644 --- a/source/mysql/types.d +++ b/source/mysql/types.d @@ -33,7 +33,7 @@ struct Timestamp ulong rep; } -union _MYTYPE +private union _MYTYPE { @safeOnly: // blobs are const because of the indirection. In this case, it's not @@ -83,6 +83,97 @@ union _MYTYPE const(.Timestamp)* TimestampRef; } +/++ +MySQLVal is MySQL's tagged algebraic type that supports only @safe usage +(see $(LINK2, http://code.dlang.org/packages/taggedalgebraic, TaggedAlgebraic) +for more information on the features of this type). Note that TaggedAlgebraic +has UFCS methods that are not available without importing that module in your +code. + +The type can hold any possible type that MySQL can use or return. The _MYTYPE +union, which is a private union for the project, defines the names of the types +that can be stored. These names double as the names for the MySQLVal.Kind +enumeration. To that end, this is the entire union definition: + +------ +private union _MYTYPE +{ + ubyte[] Blob; + const(ubyte)[] CBlob; + + typeof(null) Null; + bool Bit; + ubyte UByte; + byte Byte; + ushort UShort; + short Short; + uint UInt; + int Int; + ulong ULong; + long Long; + float Float; + double Double; + std.datetime.DateTime DateTime; + std.datetime.TimeOfDay Time; + mysql.types.Timestamp Timestamp; + std.datetime.Date Date; + + string Text; + const(char)[] CText; + + // pointers + const(bool)* BitRef; + const(ubyte)* UByteRef; + const(byte)* ByteRef; + const(ushort)* UShortRef; + const(short)* ShortRef; + const(uint)* UIntRef; + const(int)* IntRef; + const(ulong)* ULongRef; + const(long)* LongRef; + const(float)* FloatRef; + const(double)* DoubleRef; + const(DateTime)* DateTimeRef; + const(TimeOfDay)* TimeRef; + const(Date)* DateRef; + const(string)* TextRef; + const(char[])* CTextRef; + const(ubyte[])* BlobRef; + const(Timestamp)* TimestampRef; +} +------ + +Note that the pointers are all const, as the only use case in mysql-native for them is as rebindable parameters to a Prepared struct. + +MySQLVal allows operations, field, and member function access for each of the supported types without unwrapping the MySQLVal value. For example: + +------ +import mysql.safe; + +// support for comparison is valid for any type that supports it +assert(conn.queryValue("SELECT COUNT(*) FROM sometable") > 20); + +// access members of supporting types without unwrapping or verifying type first +assert(conn.queryValue("SELECT updated_date FROM someTable WHERE id=5").year == 2020); + +// arithmetic is supported, return type may vary +auto val = conn.queryValue("SELECT some_integer FROM sometable WHERE id=5") + 100; +static assert(is(typeof(val) == MySQLVal)); +assert(val.kind == MySQLVal.Kind.Int); + +// this will be a double and not a MySQLVal, because all types that support +// addition with a double result in a double. +auto val2 = conn.queryValue("SELECT some_float FROM sometable WHERE id=5") + 100.0; +static assert(is(typeof(val2) == double)); +------ + +MySQLVal is used in all operations interally for mysql-native, and explicitly +for all safe API calls. Version 3.0.x and earlier of the mysql-native library +used Variant, so this module provides multiple shims to allow code to "just +work", and also provides conversion back to Variant. + +$(SAFE_MIGRATION) ++/ alias MySQLVal = TaggedAlgebraic!_MYTYPE; // helper to convert variants to MySQLVal. Used wherever variant is still used. @@ -134,13 +225,16 @@ package MySQLVal _toVal(Variant v) } /++ -Use this as a stop-gap measure in order to keep Variant compatibility. Append this to any function which returns a MySQLVal until you can update your code. +Convert a MySQLVal into a Variant. This provides a backwards-compatible shim to use if necessary when transitioning to the safe API. + +$(SAFE_MIGRATION) +/ Variant asVariant(MySQLVal v) { return v.apply!((a) => Variant(a)); } +/// ditto Nullable!Variant asVariant(Nullable!MySQLVal v) { if(v.isNull) @@ -150,8 +244,21 @@ Nullable!Variant asVariant(Nullable!MySQLVal v) /++ Compatibility layer for MySQLVal. These functions provide methods that -TaggedAlgebraic does not provide in order to keep functionality that was -available with Variant. +$(LINK2, http://code.dlang.org/packages/taggedalgebraic, TaggedAlgebraic) +does not provide in order to keep functionality that was available with Variant. + +Notes: + +The `type` shim should be avoided in favor of using the `kind` property of +TaggedAlgebraic. + +The `get` shim works differently than the TaggedAlgebraic version, as the +Variant get function would provide implicit type conversions, but the +TaggedAlgebraic version did not. + +All shims other than `type` will likely remain as convenience features. + +$(SAFE_MIGRATION) +/ bool convertsTo(T)(ref MySQLVal val) { diff --git a/source/mysql/unsafe/commands.d b/source/mysql/unsafe/commands.d index fd475722..f4b27a15 100644 --- a/source/mysql/unsafe/commands.d +++ b/source/mysql/unsafe/commands.d @@ -24,7 +24,7 @@ import mysql.protocol.comms; import mysql.protocol.constants; import mysql.protocol.extra_types; import mysql.protocol.packets; -import mysql.unsafe.result; +import mysql.impl.result; import mysql.types; alias ColumnSpecialization = SC.ColumnSpecialization; @@ -173,21 +173,21 @@ auto rowsAffected = myConnection.exec("INSERT INTO `myTable` (`a`) VALUES (?)", +/ alias exec = SC.exec; ///ditto -ulong exec(Connection conn, const(char[]) sql, Variant[] args) +ulong exec(Connection conn, const(char[]) sql, Variant[] args) @system { auto prepared = conn.prepare(sql); prepared.setArgs(args); return exec(conn, prepared); } ///ditto -ulong exec(Connection conn, ref Prepared prepared, Variant[] args) +ulong exec(Connection conn, ref Prepared prepared, Variant[] args) @system { prepared.setArgs(args); return exec(conn, prepared); } ///ditto -ulong exec(Connection conn, ref BackwardCompatPrepared prepared) +ulong exec(Connection conn, ref BackwardCompatPrepared prepared) @system { auto p = prepared.prepared; auto result = exec(conn, p); @@ -268,32 +268,32 @@ UnsafeResultRange query(T...)(Connection conn, const(char[]) sql, T args) return SC.query(conn, sql, args).unsafe; } ///ditto -UnsafeResultRange query(Connection conn, const(char[]) sql, Variant[] args) +UnsafeResultRange query(Connection conn, const(char[]) sql, Variant[] args) @system { auto prepared = conn.prepare(sql); prepared.setArgs(args); return query(conn, prepared); } ///ditto -UnsafeResultRange query(Connection conn, ref Prepared prepared) @safe +UnsafeResultRange query(Connection conn, ref Prepared prepared) @system { return SC.query(conn, prepared).unsafe; } ///ditto -UnsafeResultRange query(T...)(Connection conn, ref Prepared prepared, T args) +UnsafeResultRange query(T...)(Connection conn, ref Prepared prepared, T args) @system if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) { return SC.query(conn, prepared, args).unsafe; } ///ditto -UnsafeResultRange query(Connection conn, ref Prepared prepared, Variant[] args) +UnsafeResultRange query(Connection conn, ref Prepared prepared, Variant[] args) @system { prepared.setArgs(args); return query(conn, prepared); } ///ditto -UnsafeResultRange query(Connection conn, ref BackwardCompatPrepared prepared) +UnsafeResultRange query(Connection conn, ref BackwardCompatPrepared prepared) @system { auto p = prepared.prepared; auto result = query(conn, p); @@ -383,12 +383,12 @@ Nullable!UnsafeRow queryRow(Connection conn, const(char[]) sql, Variant[] args) return queryRow(conn, prepared); } ///ditto -Nullable!UnsafeRow queryRow(Connection conn, ref Prepared prepared) @safe +Nullable!UnsafeRow queryRow(Connection conn, ref Prepared prepared) @system { return SC.queryRow(conn, prepared).unsafe; } ///ditto -Nullable!UnsafeRow queryRow(T...)(Connection conn, ref Prepared prepared, T args) +Nullable!UnsafeRow queryRow(T...)(Connection conn, ref Prepared prepared, T args) @system if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) { return SC.queryRow(conn, prepared, args).unsafe; @@ -401,7 +401,7 @@ Nullable!UnsafeRow queryRow(Connection conn, ref Prepared prepared, Variant[] ar } ///ditto -Nullable!UnsafeRow queryRow(Connection conn, ref BackwardCompatPrepared prepared) @safe +Nullable!UnsafeRow queryRow(Connection conn, ref BackwardCompatPrepared prepared) @system { auto p = prepared.prepared; auto result = queryRow(conn, p); @@ -437,7 +437,7 @@ args = The variables, taken by reference, to receive the values. +/ alias queryRowTuple = SC.queryRowTuple; ///ditto -void queryRowTuple(T...)(Connection conn, ref BackwardCompatPrepared prepared, ref T args) +void queryRowTuple(T...)(Connection conn, ref BackwardCompatPrepared prepared, ref T args) @system { auto p = prepared.prepared; .queryRowTuple(conn, p, args); @@ -516,7 +516,7 @@ delegate. csa = An optional array of `ColumnSpecialization` structs. If you need to use this with a prepared statement, please use `mysql.prepared.Prepared.columnSpecials`. +/ -Nullable!Variant queryValue(Connection conn, const(char[]) sql, ColumnSpecialization[] csa = null) +Nullable!Variant queryValue(Connection conn, const(char[]) sql, ColumnSpecialization[] csa = null) @system { return SC.queryValue(conn, sql, csa).asVariant; } @@ -534,12 +534,12 @@ Nullable!Variant queryValue(Connection conn, const(char[]) sql, Variant[] args) return queryValue(conn, prepared); } ///ditto -Nullable!Variant queryValue(Connection conn, ref Prepared prepared) +Nullable!Variant queryValue(Connection conn, ref Prepared prepared) @system { return SC.queryValue(conn, prepared).asVariant; } ///ditto -Nullable!Variant queryValue(T...)(Connection conn, ref Prepared prepared, T args) +Nullable!Variant queryValue(T...)(Connection conn, ref Prepared prepared, T args) @system if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) { return SC.queryValue(conn, prepared, args).asVariant; @@ -551,7 +551,7 @@ Nullable!Variant queryValue(Connection conn, ref Prepared prepared, Variant[] ar return queryValue(conn, prepared); } ///ditto -Nullable!Variant queryValue(Connection conn, ref BackwardCompatPrepared prepared) +Nullable!Variant queryValue(Connection conn, ref BackwardCompatPrepared prepared) @system { auto p = prepared.prepared; auto result = queryValue(conn, p); diff --git a/source/mysql/unsafe/connection.d b/source/mysql/unsafe/connection.d index f2845f1a..3d4856c6 100644 --- a/source/mysql/unsafe/connection.d +++ b/source/mysql/unsafe/connection.d @@ -1,21 +1,41 @@ +/++ +Connect to a MySQL/MariaDB server. + +This is the unsafe API for the Connection type. It publicly imports `mysql.impl.connection`, and also provides the unsafe version of the API for preparing statements. + +Note that unsafe prepared statements are no different from safe prepared statements, except for the mechanism to set parameters allows Variant. + +This module also contains the soon-to-be-deprecated BackwardCompatPrepared type. + +$(SAFE_MIGRATION) ++/ module mysql.unsafe.connection; +public import mysql.impl.connection; import mysql.unsafe.prepared; import mysql.unsafe.commands; -public import mysql.internal.connection; private import CS = mysql.safe.connection; -Prepared prepare(Connection conn, const(char[]) sql) @safe +/++ +Convenience functions. + +Returns: an UnsafePrepared instance based on the result of the corresponding `mysql.safe.connection` function. + +See that module for more details on how these functions work. ++/ +UnsafePrepared prepare(Connection conn, const(char[]) sql) @safe { return CS.prepare(conn, sql).unsafe; } -Prepared prepareFunction(Connection conn, string name, int numArgs) @safe +/// ditto +UnsafePrepared prepareFunction(Connection conn, string name, int numArgs) @safe { return CS.prepareFunction(conn, name, numArgs).unsafe; } -Prepared prepareProcedure(Connection conn, string name, int numArgs) @safe +/// ditto +UnsafePrepared prepareProcedure(Connection conn, string name, int numArgs) @safe { return CS.prepareProcedure(conn, name, numArgs).unsafe; } @@ -38,7 +58,7 @@ package(mysql) BackwardCompatPrepared prepareBackwardCompatImpl(Connection conn, } /++ -This is a wrapper over `mysql.prepared.Prepared`, provided ONLY as a +This is a wrapper over `mysql.unsafe.prepared.Prepared`, provided ONLY as a temporary aid in upgrading to mysql-native v2.0.0 and its new connection-independent model of prepared statements. See the $(LINK2 https://github.com/mysql-d/mysql-native/blob/master/MIGRATING_TO_V2.md, migration guide) @@ -111,21 +131,21 @@ struct BackwardCompatPrepared See `BackwardCompatPrepared` for more info. +/ deprecated("Change 'preparedStmt.exec()' to 'conn.exec(preparedStmt)'") - ulong exec() @safe + ulong exec() @system { return .exec(_conn, _prepared); } ///ditto deprecated("Change 'preparedStmt.query()' to 'conn.query(preparedStmt)'") - ResultRange query() @safe + ResultRange query() @system { return .query(_conn, _prepared); } ///ditto deprecated("Change 'preparedStmt.queryRow()' to 'conn.queryRow(preparedStmt)'") - Nullable!Row queryRow() @safe + Nullable!Row queryRow() @system { return .queryRow(_conn, _prepared); } diff --git a/source/mysql/unsafe/package.d b/source/mysql/unsafe/package.d index 686bccc7..c40a30d2 100644 --- a/source/mysql/unsafe/package.d +++ b/source/mysql/unsafe/package.d @@ -3,6 +3,8 @@ Imports all of $(LINK2 https://github.com/mysql-d/mysql-native, mysql-native). This module will import all modules that use the unsafe API of the mysql library. Please import `mysql.safe` for the safe version. + +$(SAFE_MIGRATION) +/ module mysql.unsafe; @@ -10,9 +12,9 @@ public import mysql.unsafe.commands; public import mysql.unsafe.result; public import mysql.unsafe.pool; public import mysql.unsafe.prepared; +public import mysql.unsafe.connection; // common imports -public import mysql.connection; public import mysql.escape; public import mysql.exceptions; public import mysql.metadata; diff --git a/source/mysql/unsafe/pool.d b/source/mysql/unsafe/pool.d index 698cd3d6..f4d2b80f 100644 --- a/source/mysql/unsafe/pool.d +++ b/source/mysql/unsafe/pool.d @@ -1,6 +1,20 @@ +/++ +Connect to a MySQL/MariaDB database using vibe.d's +$(LINK2 http://vibed.org/api/vibe.core.connectionpool/ConnectionPool, ConnectionPool). + +This aliases `mysql.impl.pool.MySQLPoolImpl!false` as MySQLPool. Please see the +`mysql.impl.pool` moddule for documentation on how to use MySQLPool. + +This is the unsafe version of mysql's pool module, and as such uses only @system +callback delegates. If you wish to use @safe callbacks, import +`mysql.safe.pool`. + +$(SAFE_MIGRATION) ++/ + module mysql.unsafe.pool; -import mysql.internal.pool; +import mysql.impl.pool; // need to check if mysqlpool was enabled static if(__traits(compiles, () { alias p = MySQLPoolImpl!false; })) alias MySQLPool = MySQLPoolImpl!false; diff --git a/source/mysql/unsafe/prepared.d b/source/mysql/unsafe/prepared.d index 9882868a..8dfa8e72 100644 --- a/source/mysql/unsafe/prepared.d +++ b/source/mysql/unsafe/prepared.d @@ -1,6 +1,22 @@ +/++ +This module publicly imports `mysql.impl.prepared`. See that module for +documentation on using prepared statements with a MySQL server. + +This module also aliases the unsafe versions of structs to the original struct +names to aid in backwards compatibility. + +$(SAFE_MIGRATION) +++/ module mysql.unsafe.prepared; -public import mysql.internal.prepared; +public import mysql.impl.prepared; + +/++ +Unsafe aliases. Use these instead of the real name. See the documentation on +the aliased types for usage. +++/ alias Prepared = UnsafePrepared; +/// ditto alias ParameterSpecialization = UnsafeParameterSpecialization; +/// ditto alias PSN = UnsafeParameterSpecialization; diff --git a/source/mysql/unsafe/result.d b/source/mysql/unsafe/result.d index 870acd78..a993a365 100644 --- a/source/mysql/unsafe/result.d +++ b/source/mysql/unsafe/result.d @@ -1,6 +1,19 @@ +/++ +This module publicly imports `mysql.impl.result`. See that module for documentation on how to use result and result range structures. + +This module also aliases the unsafe versions of structs to the original struct +names to aid in backwards compatibility. + +$(SAFE_MIGRATION) ++/ module mysql.unsafe.result; -public import mysql.internal.result; +public import mysql.impl.result; +/++ +Unsafe aliases. Use these instead of the real name. See the documentation on +the aliased types for usage. ++/ alias Row = UnsafeRow; +/// ditto alias ResultRange = UnsafeResultRange; From 2ff1a964370129aa66262a2a1566e861115e1017 Mon Sep 17 00:00:00 2001 From: Steven Schveighoffer Date: Fri, 21 Feb 2020 11:45:30 -0500 Subject: [PATCH 12/14] Minor doc fixes. --- SAFE_MIGRATION.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SAFE_MIGRATION.md b/SAFE_MIGRATION.md index 4c3f3760..5f0b2159 100644 --- a/SAFE_MIGRATION.md +++ b/SAFE_MIGRATION.md @@ -8,7 +8,7 @@ First, note that the latest version of mysql, while it supports a safe API, is d *related: please see D's [Memory Safety](https://dlang.org/spec/memory-safe-d.html) page to understand what `@safe` does in D* -Since mysql-native is intended to be a key component of servers that are on the Internet, it must support the capability (even if not required) to be fully `@safe`. In addition, major web frameworks (e.g. [vibe.d](http://code.dlang.org/packages/vibe-d)) and arguably any other program is headed in this direction. +Since mysql-native is intended to be a key component of servers that are on the Internet, it must support the capability (even if not required) to be fully `@safe`. In addition, major web frameworks (e.g. [vibe.d](http://code.dlang.org/packages/vibe-d)) and arguably any other program are headed in this direction. In other words, the world wants memory safe code, and libraries that provide safe interfaces and guarantees will be much more appealing. It's just not acceptable any more for the components of major development projects to be careless about memory safety. @@ -127,7 +127,7 @@ struct EstablishedStruct void fetchFromDatabase(Connection conn) { // all safe calls except asVariant - value conn.queryValue("SELECT value FROM theTable WHERE id = ?", id).asVariant; + value = conn.queryValue("SELECT value FROM theTable WHERE id = ?", id).asVariant; } } ``` @@ -162,7 +162,7 @@ Even in cases where you elect to defer updating code, you can still import the ` We recommend following these steps to transition. In most cases, you should see very little breakage of code: -1. Adjust your imports to import the safe versions of mysql moduels. If you import the `mysql` package, instead import the `mysql.safe` package. If you import any of the individual modules listed in the [API](#The safe/unsafe API) section, use the `mysql.safe.modulename` equivalent instead. +1. Adjust your imports to import the safe versions of mysql modules. If you import the `mysql` package, instead import the `mysql.safe` package. If you import any of the individual modules listed in the [API](#the-safeunsafe-api) section, use the `mysql.safe.modulename` equivalent instead. 2. Adjust any explicit uses of `Variant` to `MySQLVal` or use `auto` for type inference. Remember that variables typed as `Variant` explicitly will consume `MySQLVal`, so you may not get compiler errors for these, but you will certainly get runtime errors. 3. If there are cases where you cannot stop using `Variant`, use the `asVariant` compatibility shim. 4. Adjust uses of `Variant`'s methods to use the `TaggedAlgebraic` versions. Most important is usage of the `kind` member, as comparing two `TypeInfo` objects is currently `@system`. From f7000919a283b4468869646b2c21de1c21bcec71 Mon Sep 17 00:00:00 2001 From: Steven Schveighoffer Date: Sun, 19 Apr 2020 14:44:17 -0400 Subject: [PATCH 13/14] Now builds docs (with latest version of compiler if gen-package-version is updated) --- .gitignore | 1 + build-docs | 10 ++++++++-- examples/homePage/dub.selections.json | 1 + source/mysql/commands.d | 7 +------ source/mysql/connection.d | 12 ++---------- source/mysql/impl/connection.d | 6 +++--- source/mysql/impl/pool.d | 2 ++ source/mysql/impl/prepared.d | 2 +- source/mysql/impl/result.d | 2 +- source/mysql/package.d | 14 ++++++++++++++ source/mysql/pool.d | 6 +----- source/mysql/result.d | 10 ++-------- source/mysql/safe/commands.d | 2 ++ source/mysql/safe/connection.d | 13 +++++++++---- source/mysql/safe/package.d | 2 +- source/mysql/safe/pool.d | 6 +++--- source/mysql/safe/prepared.d | 4 ++-- source/mysql/safe/result.d | 2 +- source/mysql/unsafe/commands.d | 15 ++++++++++++--- source/mysql/unsafe/connection.d | 11 ++++++++--- source/mysql/unsafe/package.d | 2 +- source/mysql/unsafe/pool.d | 6 +++--- source/mysql/unsafe/prepared.d | 4 ++-- source/mysql/unsafe/result.d | 2 +- 24 files changed, 82 insertions(+), 60 deletions(-) diff --git a/.gitignore b/.gitignore index 9c6a5f88..97e967df 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ /docs/docs.json /docs/public/mysql/ +/docs/public/taggedalgebraic/ /docs/public/file_hashes.json /docs/public/index.html /docs/public/mysql.html diff --git a/build-docs b/build-docs index 8d99abb3..c53afde7 100755 --- a/build-docs +++ b/build-docs @@ -16,12 +16,18 @@ rm -rf docs_tmp ta rm source/mysql/package.o dub --version -dub run gen-package-version -- mysql --ddoc=ddoc --src=source +if ! dub run gen-package-version -- mysql --ddoc=ddoc --src=source; then + echo "Failed to build gen-package-version" + exit 1 +fi rm source/mysql/packageVersion.d echo Building ddox... cd ./ddox -dub build +if ! dub build; then + echo "Failed to build ddox" + exit 1 +fi cd .. echo Done building ddox... diff --git a/examples/homePage/dub.selections.json b/examples/homePage/dub.selections.json index fe39fbbb..af853093 100644 --- a/examples/homePage/dub.selections.json +++ b/examples/homePage/dub.selections.json @@ -6,6 +6,7 @@ "libev": "5.0.0+4.04", "libevent": "2.0.1+2.0.16", "memutils": "0.4.13", + "mysql-native": {"path":"../.."}, "openssl": "1.1.4+1.0.1g", "stdx-allocator": "2.77.5", "taggedalgebraic": "0.11.8", diff --git a/source/mysql/commands.d b/source/mysql/commands.d index b9f48203..86afba3d 100644 --- a/source/mysql/commands.d +++ b/source/mysql/commands.d @@ -1,10 +1,5 @@ /++ -This module publicly imports `mysql.unsafe.commands`, which provides the -Variant-based interface to mysql. In the future, this will switch to importing the -`mysql.safe.commands`, which provides the @safe interface to mysql. Please see -those two modules for documentation on the functions provided. It is highly -recommended to import `mysql.safe.commands` and not the unsafe commands, as -that is the future for mysql-native. +This module publicly imports `mysql.unsafe.commands`. Please see that module for more documentation. In the far future, the unsafe version will be deprecated and removed, and the safe version moved to this location. diff --git a/source/mysql/connection.d b/source/mysql/connection.d index ae64d751..577dbeaa 100644 --- a/source/mysql/connection.d +++ b/source/mysql/connection.d @@ -1,14 +1,6 @@ /++ -This module publicly imports `mysql.unsafe.connection`, which provides -backwards compatible functions for connecting to a MySQL/MariaDB server. - -It is recommended instead to import `mysql.safe.connection`, which provides -@safe-only mechanisms to connect to a database. - -Note that the common pieces of the connection are documented and currently -reside in `mysql.impl.connection`. The safe and unsafe portions of the API -reside in `mysql.unsafe.connection` and `mysql.safe.connection` respectively. -Please see these modules for information on using a MySQL `Connection` object. +This module publicly imports `mysql.unsafe.connection`. Please see that module +for more documentation. In the future, this will migrate to importing `mysql.safe.connection`. In the far future, the unsafe version will be deprecated and removed, and the safe diff --git a/source/mysql/impl/connection.d b/source/mysql/impl/connection.d index a14f06f7..5232e35d 100644 --- a/source/mysql/impl/connection.d +++ b/source/mysql/impl/connection.d @@ -1,5 +1,5 @@ /++ -Connect to a MySQL/MariaDB server. +Implementation - Connection class. WARNING: This module is used to consolidate the common implementation of the safe and @@ -1132,7 +1132,7 @@ unittest cn.exec("DROP TABLE `dropConnection`"); } -/+ +/* Test Prepared's ability to be safely refcount-released during a GC cycle (ie, `Connection.release` must not allocate GC memory). @@ -1140,7 +1140,7 @@ Currently disabled because it's not guaranteed to always work (and apparently, cannot be made to work?) For relevant discussion, see issue #159: https://github.com/mysql-d/mysql-native/issues/159 -+/ +*/ version(none) debug(MYSQLN_TESTS) { diff --git a/source/mysql/impl/pool.d b/source/mysql/impl/pool.d index 5ddd125b..c170b7ea 100644 --- a/source/mysql/impl/pool.d +++ b/source/mysql/impl/pool.d @@ -1,4 +1,6 @@ /++ +Implementation - Vibe.d MySQL Pool. + Connect to a MySQL/MariaDB database using vibe.d's $(LINK2 http://vibed.org/api/vibe.core.connectionpool/ConnectionPool, ConnectionPool). diff --git a/source/mysql/impl/prepared.d b/source/mysql/impl/prepared.d index e0204f56..07f9c17c 100644 --- a/source/mysql/impl/prepared.d +++ b/source/mysql/impl/prepared.d @@ -1,5 +1,5 @@ /++ -Use a DB via SQL prepared statements. +Implementation - Prepared statements. WARNING: This module is used to consolidate the common implementation of the safe and diff --git a/source/mysql/impl/result.d b/source/mysql/impl/result.d index 3d24de40..01f11dd4 100644 --- a/source/mysql/impl/result.d +++ b/source/mysql/impl/result.d @@ -1,5 +1,5 @@ /++ -Structures for data received: rows and result sets (ie, a range of rows). +Implementation - Data result structures. WARNING: This module is used to consolidate the common implementation of the safe and diff --git a/source/mysql/package.d b/source/mysql/package.d index a981570d..52fb0bb6 100644 --- a/source/mysql/package.d +++ b/source/mysql/package.d @@ -75,6 +75,20 @@ module mysql; // by default we do the unsafe API. public import mysql.unsafe; +// import the safe version for generating documentation. +version(MySQLDocs) +{ + // also document safe items + import mysql.safe; + + // also document forwarding modules + import mysql.commands; + import mysql.result; + import mysql.pool; + import mysql.prepared; + import mysql.connection; +} + debug(MYSQLN_TESTS) version = DoCoreTests; debug(MYSQLN_CORE_TESTS) version = DoCoreTests; diff --git a/source/mysql/pool.d b/source/mysql/pool.d index 1a879673..8cd50845 100644 --- a/source/mysql/pool.d +++ b/source/mysql/pool.d @@ -1,9 +1,5 @@ /++ -This module publicly imports `mysql.unsafe.pool`, which provides backwards -compatible functions for using vibe.d's -$(LINK2 http://vibed.org/api/vibe.core.connectionpool/ConnectionPool, ConnectionPool). - -Please see the module documentation in `mysql.impl.pool` for more details. +This module publicly imports `mysql.unsafe.pool`. Please see that module for more documentation. In the future, this will migrate to importing `mysql.safe.pool`. In the far future, the unsafe version will be deprecated and removed, and the safe version diff --git a/source/mysql/result.d b/source/mysql/result.d index e793a070..b47ea376 100644 --- a/source/mysql/result.d +++ b/source/mysql/result.d @@ -1,12 +1,6 @@ /++ -This module publicly imports `mysql.unsafe.result`, which provides backwards -compatible structures for processing rows of data from a MySQL server. Please -see that module for details on usage. - -It is recommended instead ot import `mysql.safe.result`, which provides -@safe-only mechanisms for processing rows of data. - -Note that the actual structs are documented in `mysql.impl.result`. +This module publicly imports `mysql.unsafe.result`. Please see that module for +more documentation. In the future, this will migrate to importing `mysql.safe.result`. In the far future, the unsafe version will be deprecated and removed, and the safe version diff --git a/source/mysql/safe/commands.d b/source/mysql/safe/commands.d index a71f84bb..195bed7c 100644 --- a/source/mysql/safe/commands.d +++ b/source/mysql/safe/commands.d @@ -1,4 +1,6 @@ /++ +Use a DB via plain SQL statements (safe version). + Commands that are expected to return a result set - queries - have distinctive methods that are enforced. That is it will be an error to call such a method with an SQL command that does not produce a result set. So for commands like diff --git a/source/mysql/safe/connection.d b/source/mysql/safe/connection.d index df7b2585..0f091ca7 100644 --- a/source/mysql/safe/connection.d +++ b/source/mysql/safe/connection.d @@ -1,8 +1,12 @@ /++ -Connect to a MySQL/MariaDB server. +Connect to a MySQL/MariaDB server (safe version). This is the @safe API for the Connection type. It publicly imports `mysql.impl.connection`, and also provides the safe version of the API for preparing statements. +Note that the common pieces of the connection are documented and currently +reside in `mysql.impl.connection`. Please see this module for documentation of +the connection object. + $(SAFE_MIGRATION) +/ module mysql.safe.connection; @@ -18,9 +22,10 @@ import mysql.safe.commands; Submit an SQL command to the server to be compiled into a prepared statement. This will automatically register the prepared statement on the provided connection. -The resulting `mysql.impl.prepared.SafePrepared` can then be used freely on ANY `Connection`, -as it will automatically be registered upon its first use on other connections. -Or, pass it to `Connection.register` if you prefer eager registration. +The resulting `mysql.impl.prepared.SafePrepared` can then be used freely on ANY +`mysql.impl.connection.Connection`, as it will automatically be registered upon +its first use on other connections. Or, pass it to +`mysql.impl.connection.Connection.register` if you prefer eager registration. Internally, the result of a successful outcome will be a statement handle - an ID - for the prepared statement, a count of the parameters required for diff --git a/source/mysql/safe/package.d b/source/mysql/safe/package.d index ddc331c9..c9fcf1c0 100644 --- a/source/mysql/safe/package.d +++ b/source/mysql/safe/package.d @@ -1,5 +1,5 @@ /++ -Imports all of $(LINK2 https://github.com/mysql-d/mysql-native, mysql-native). +Imports all of $(LINK2 https://github.com/mysql-d/mysql-native, mysql-native) (safe versions). This module will import all modules that use the safe API of the mysql library. In a future version, this will become the default. diff --git a/source/mysql/safe/pool.d b/source/mysql/safe/pool.d index 7455ccd4..2269a52f 100644 --- a/source/mysql/safe/pool.d +++ b/source/mysql/safe/pool.d @@ -1,9 +1,9 @@ /++ Connect to a MySQL/MariaDB database using vibe.d's -$(LINK2 http://vibed.org/api/vibe.core.connectionpool/ConnectionPool, ConnectionPool). +$(LINK2 http://vibed.org/api/vibe.core.connectionpool/ConnectionPool, ConnectionPool) (safe version). -This aliases `mysql.impl.pool.MySQLPoolImpl!true` as MySQLPool. Please see the -`mysql.impl.pool` moddule for documentation on how to use MySQLPool. +This aliases `mysql.impl.pool.MySQLPoolImpl!true` as `MySQLPool`. Please see the +`mysql.impl.pool` moddule for documentation on how to use `MySQLPool`. This is the @safe version of mysql's pool module, and as such uses only @safe callback delegates. If you wish to use @system callbacks, import diff --git a/source/mysql/safe/prepared.d b/source/mysql/safe/prepared.d index cc860a23..aafbfe2e 100644 --- a/source/mysql/safe/prepared.d +++ b/source/mysql/safe/prepared.d @@ -1,6 +1,6 @@ /++ -This module publicly imports `mysql.impl.prepared`. See that module for -documentation on using prepared statements with a MySQL server. +This module publicly imports `mysql.impl.prepared` (safe version). See that +module for documentation on using prepared statements with a MySQL server. This module also aliases the safe versions of structs to the original struct names to aid in transitioning to using safe code with minimal impact. diff --git a/source/mysql/safe/result.d b/source/mysql/safe/result.d index d416d122..96e4e5b8 100644 --- a/source/mysql/safe/result.d +++ b/source/mysql/safe/result.d @@ -1,5 +1,5 @@ /++ -This module publicly imports `mysql.impl.result`. See that module for documentation on how to use result and result range structures. +This module publicly imports `mysql.impl.result`. See that module for documentation on how to use result and result range structures (safe versions). This module also aliases the safe versions of these structs to the original struct names to aid in transitioning to using safe code with minimal impact. diff --git a/source/mysql/unsafe/commands.d b/source/mysql/unsafe/commands.d index f4b27a15..5c5db712 100644 --- a/source/mysql/unsafe/commands.d +++ b/source/mysql/unsafe/commands.d @@ -1,11 +1,15 @@ /++ -Use a DB via plain SQL statements. +Use a DB via plain SQL statements (unsafe version). Commands that are expected to return a result set - queries - have distinctive methods that are enforced. That is it will be an error to call such a method with an SQL command that does not produce a result set. So for commands like SELECT, use the `query` functions. For other commands, like INSERT/UPDATE/CREATE/etc, use `exec`. + +This is the @system version of mysql's command module, and as such uses the @system +rows and result ranges, and the `Variant` type. For the `MySQLVal` safe +version, please import `mysql.safe.commands`. +/ module mysql.unsafe.commands; @@ -130,6 +134,9 @@ unittest /++ Execute an SQL command or prepared statement, such as INSERT/UPDATE/CREATE/etc. +Note: The safe `mysql.safe.commands.exec` is also aliased here, so you have access to all +those overloads in addition to these. + This method is intended for commands such as which do not produce a result set (otherwise, use one of the `query` functions instead.) If the SQL command does produces a result set (such as SELECT), `mysql.exceptions.MYXResultRecieved` @@ -171,8 +178,6 @@ auto myInt = 7; auto rowsAffected = myConnection.exec("INSERT INTO `myTable` (`a`) VALUES (?)", myInt); --- +/ -alias exec = SC.exec; -///ditto ulong exec(Connection conn, const(char[]) sql, Variant[] args) @system { auto prepared = conn.prepare(sql); @@ -195,6 +200,10 @@ ulong exec(Connection conn, ref BackwardCompatPrepared prepared) @system return result; } +//ditto +// Note: doesn't look right in ddox, so I removed this as a ditto. +alias exec = SC.exec; + /++ Execute an SQL SELECT command or prepared statement. diff --git a/source/mysql/unsafe/connection.d b/source/mysql/unsafe/connection.d index 3d4856c6..65dcb282 100644 --- a/source/mysql/unsafe/connection.d +++ b/source/mysql/unsafe/connection.d @@ -1,9 +1,14 @@ /++ -Connect to a MySQL/MariaDB server. +Connect to a MySQL/MariaDB server (unsafe version). -This is the unsafe API for the Connection type. It publicly imports `mysql.impl.connection`, and also provides the unsafe version of the API for preparing statements. +This is the unsafe API for the Connection type. It publicly imports +`mysql.impl.connection`, and also provides the unsafe version of the API for +preparing statements. Note that unsafe prepared statements actually use safe +code underneath. -Note that unsafe prepared statements are no different from safe prepared statements, except for the mechanism to set parameters allows Variant. +Note that the common pieces of the connection are documented and currently +reside in `mysql.impl.connection`. Please see this module for documentation of +the connection object. This module also contains the soon-to-be-deprecated BackwardCompatPrepared type. diff --git a/source/mysql/unsafe/package.d b/source/mysql/unsafe/package.d index c40a30d2..bc84821c 100644 --- a/source/mysql/unsafe/package.d +++ b/source/mysql/unsafe/package.d @@ -1,5 +1,5 @@ /++ -Imports all of $(LINK2 https://github.com/mysql-d/mysql-native, mysql-native). +Imports all of $(LINK2 https://github.com/mysql-d/mysql-native, mysql-native) (unsafe versions). This module will import all modules that use the unsafe API of the mysql library. Please import `mysql.safe` for the safe version. diff --git a/source/mysql/unsafe/pool.d b/source/mysql/unsafe/pool.d index f4d2b80f..f97692f2 100644 --- a/source/mysql/unsafe/pool.d +++ b/source/mysql/unsafe/pool.d @@ -1,9 +1,9 @@ /++ Connect to a MySQL/MariaDB database using vibe.d's -$(LINK2 http://vibed.org/api/vibe.core.connectionpool/ConnectionPool, ConnectionPool). +$(LINK2 http://vibed.org/api/vibe.core.connectionpool/ConnectionPool, ConnectionPool) (unsafe version). -This aliases `mysql.impl.pool.MySQLPoolImpl!false` as MySQLPool. Please see the -`mysql.impl.pool` moddule for documentation on how to use MySQLPool. +This aliases `mysql.impl.pool.MySQLPoolImpl!false` as `MySQLPool`. Please see the +`mysql.impl.pool` moddule for documentation on how to use `MySQLPool`. This is the unsafe version of mysql's pool module, and as such uses only @system callback delegates. If you wish to use @safe callbacks, import diff --git a/source/mysql/unsafe/prepared.d b/source/mysql/unsafe/prepared.d index 8dfa8e72..01a8c257 100644 --- a/source/mysql/unsafe/prepared.d +++ b/source/mysql/unsafe/prepared.d @@ -1,6 +1,6 @@ /++ -This module publicly imports `mysql.impl.prepared`. See that module for -documentation on using prepared statements with a MySQL server. +This module publicly imports `mysql.impl.prepared` (unsafe version). See that +module for documentation on using prepared statements with a MySQL server. This module also aliases the unsafe versions of structs to the original struct names to aid in backwards compatibility. diff --git a/source/mysql/unsafe/result.d b/source/mysql/unsafe/result.d index a993a365..2d0a7f80 100644 --- a/source/mysql/unsafe/result.d +++ b/source/mysql/unsafe/result.d @@ -1,5 +1,5 @@ /++ -This module publicly imports `mysql.impl.result`. See that module for documentation on how to use result and result range structures. +This module publicly imports `mysql.impl.result`. See that module for documentation on how to use result and result range structures (unsafe versions). This module also aliases the unsafe versions of structs to the original struct names to aid in backwards compatibility. From fa231b1aef778eb098ac48a911db70d094a036e7 Mon Sep 17 00:00:00 2001 From: Steven Schveighoffer Date: Wed, 29 Apr 2020 23:08:19 -0400 Subject: [PATCH 14/14] Run all relevant tests using both safe and unsafe API. Fix issues with MySQLVal and related functions caught by full test runs. --- source/mysql/exceptions.d | 2 +- source/mysql/impl/connection.d | 474 +++++---- source/mysql/impl/pool.d | 69 +- source/mysql/impl/prepared.d | 461 +++++---- source/mysql/impl/result.d | 46 +- source/mysql/safe/commands.d | 2 +- source/mysql/safe/connection.d | 2 +- source/mysql/test/common.d | 14 +- source/mysql/test/integration.d | 1694 ++++++++++++++++--------------- source/mysql/test/regression.d | 446 ++++---- source/mysql/types.d | 30 +- source/mysql/unsafe/commands.d | 60 +- 12 files changed, 1841 insertions(+), 1459 deletions(-) diff --git a/source/mysql/exceptions.d b/source/mysql/exceptions.d index 9842569e..560e1b69 100644 --- a/source/mysql/exceptions.d +++ b/source/mysql/exceptions.d @@ -162,7 +162,7 @@ debug(MYSQLN_TESTS) unittest { import std.exception; - import mysql.safe.commands; + import mysql.commands; import mysql.connection; import mysql.prepared; import mysql.test.common : scopedCn, createCn; diff --git a/source/mysql/impl/connection.d b/source/mysql/impl/connection.d index 5232e35d..6b2911de 100644 --- a/source/mysql/impl/connection.d +++ b/source/mysql/impl/connection.d @@ -552,66 +552,73 @@ public: // ResultRange doesn't get invalidated upon reconnect @("reconnect") debug(MYSQLN_TESTS) - unittest + @system unittest { - import std.variant; - import mysql.safe.commands; - mixin(scopedCn); - cn.exec("DROP TABLE IF EXISTS `reconnect`"); - cn.exec("CREATE TABLE `reconnect` (a INTEGER) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - cn.exec("INSERT INTO `reconnect` VALUES (1),(2),(3)"); - - enum sql = "SELECT a FROM `reconnect`"; - - // Sanity check - auto rows = cn.query(sql).array; - assert(rows[0][0] == 1); - assert(rows[1][0] == 2); - assert(rows[2][0] == 3); - - // Ensure reconnect keeps the same connection when it's supposed to - auto range = cn.query(sql); - assert(range.front[0] == 1); - cn.reconnect(); - assert(!cn.closed); // Is open? - assert(range.isValid); // Still valid? - range.popFront(); - assert(range.front[0] == 2); - - // Ensure reconnect reconnects when it's supposed to - range = cn.query(sql); - assert(range.front[0] == 1); - cn._clientCapabilities = ~cn._clientCapabilities; // Pretend that we're changing the clientCapabilities - cn.reconnect(~cn._clientCapabilities); - assert(!cn.closed); // Is open? - assert(!range.isValid); // Was invalidated? - cn.query(sql).array; // Connection still works? - - // Try manually reconnecting - range = cn.query(sql); - assert(range.front[0] == 1); - cn.connect(cn._clientCapabilities); - assert(!cn.closed); // Is open? - assert(!range.isValid); // Was invalidated? - cn.query(sql).array; // Connection still works? - - // Try manually closing and connecting - range = cn.query(sql); - assert(range.front[0] == 1); - cn.close(); - assert(cn.closed); // Is closed? - assert(!range.isValid); // Was invalidated? - cn.connect(cn._clientCapabilities); - assert(!cn.closed); // Is open? - assert(!range.isValid); // Was invalidated? - cn.query(sql).array; // Connection still works? - - // Auto-reconnect upon a command - cn.close(); - assert(cn.closed); - range = cn.query(sql); - assert(!cn.closed); - assert(range.front[0] == 1); + static void test(bool doSafe)() + { + static if(doSafe) + import mysql.safe.commands; + else + import mysql.unsafe.commands; + mixin(scopedCn); + cn.exec("DROP TABLE IF EXISTS `reconnect`"); + cn.exec("CREATE TABLE `reconnect` (a INTEGER) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + cn.exec("INSERT INTO `reconnect` VALUES (1),(2),(3)"); + + enum sql = "SELECT a FROM `reconnect`"; + + // Sanity check + auto rows = cn.query(sql).array; + assert(rows[0][0] == 1); + assert(rows[1][0] == 2); + assert(rows[2][0] == 3); + + // Ensure reconnect keeps the same connection when it's supposed to + auto range = cn.query(sql); + assert(range.front[0] == 1); + cn.reconnect(); + assert(!cn.closed); // Is open? + assert(range.isValid); // Still valid? + range.popFront(); + assert(range.front[0] == 2); + + // Ensure reconnect reconnects when it's supposed to + range = cn.query(sql); + assert(range.front[0] == 1); + cn._clientCapabilities = ~cn._clientCapabilities; // Pretend that we're changing the clientCapabilities + cn.reconnect(~cn._clientCapabilities); + assert(!cn.closed); // Is open? + assert(!range.isValid); // Was invalidated? + cn.query(sql).array; // Connection still works? + + // Try manually reconnecting + range = cn.query(sql); + assert(range.front[0] == 1); + cn.connect(cn._clientCapabilities); + assert(!cn.closed); // Is open? + assert(!range.isValid); // Was invalidated? + cn.query(sql).array; // Connection still works? + + // Try manually closing and connecting + range = cn.query(sql); + assert(range.front[0] == 1); + cn.close(); + assert(cn.closed); // Is closed? + assert(!range.isValid); // Was invalidated? + cn.connect(cn._clientCapabilities); + assert(!cn.closed); // Is open? + assert(!range.isValid); // Was invalidated? + cn.query(sql).array; // Connection still works? + + // Auto-reconnect upon a command + cn.close(); + assert(cn.closed); + range = cn.query(sql); + assert(!cn.closed); + assert(range.front[0] == 1); + } + test!false(); + () @safe { test!true(); } (); } private void quit() @@ -819,6 +826,12 @@ public: register(prepared.sql); } + ///ditto + void register(UnsafePrepared prepared) + { + register(prepared.sql); + } + ///ditto void register(const(char[]) sql) { @@ -869,6 +882,12 @@ public: release(prepared.sql); } + ///ditto + void release(UnsafePrepared prepared) + { + release(prepared.sql); + } + ///ditto void release(const(char[]) sql) { @@ -926,32 +945,44 @@ public: @("releaseAll") debug(MYSQLN_TESTS) - unittest + @system unittest { - import mysql.safe.commands; - import mysql.safe.connection; - mixin(scopedCn); - - cn.exec("DROP TABLE IF EXISTS `releaseAll`"); - cn.exec("CREATE TABLE `releaseAll` (a INTEGER) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - - auto preparedSelect = cn.prepare("SELECT * FROM `releaseAll`"); - auto preparedInsert = cn.prepare("INSERT INTO `releaseAll` (a) VALUES (1)"); - assert(cn.isRegistered(preparedSelect)); - assert(cn.isRegistered(preparedInsert)); - - cn.releaseAll(); - assert(!cn.isRegistered(preparedSelect)); - assert(!cn.isRegistered(preparedInsert)); - cn.exec("INSERT INTO `releaseAll` (a) VALUES (1)"); - assert(!cn.isRegistered(preparedSelect)); - assert(!cn.isRegistered(preparedInsert)); - - cn.exec(preparedInsert); - cn.query(preparedSelect).array; - assert(cn.isRegistered(preparedSelect)); - assert(cn.isRegistered(preparedInsert)); - + static void test(bool doSafe)() + { + static if(doSafe) + { + import mysql.safe.commands; + import mysql.safe.connection; + } + else + { + import mysql.unsafe.commands; + import mysql.unsafe.connection; + } + mixin(scopedCn); + + cn.exec("DROP TABLE IF EXISTS `releaseAll`"); + cn.exec("CREATE TABLE `releaseAll` (a INTEGER) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + + auto preparedSelect = cn.prepare("SELECT * FROM `releaseAll`"); + auto preparedInsert = cn.prepare("INSERT INTO `releaseAll` (a) VALUES (1)"); + assert(cn.isRegistered(preparedSelect)); + assert(cn.isRegistered(preparedInsert)); + + cn.releaseAll(); + assert(!cn.isRegistered(preparedSelect)); + assert(!cn.isRegistered(preparedInsert)); + cn.exec("INSERT INTO `releaseAll` (a) VALUES (1)"); + assert(!cn.isRegistered(preparedSelect)); + assert(!cn.isRegistered(preparedInsert)); + + cn.exec(preparedInsert); + cn.query(preparedSelect).array; + assert(cn.isRegistered(preparedSelect)); + assert(cn.isRegistered(preparedInsert)); + } + test!false(); + () @safe { test!true(); } (); } /// Is the given statement registered on this connection as a prepared statement? @@ -960,6 +991,12 @@ public: return isRegistered( prepared.sql ); } + ///ditto + bool isRegistered(UnsafePrepared prepared) + { + return isRegistered( prepared.sql ); + } + ///ditto bool isRegistered(const(char[]) sql) { @@ -976,131 +1013,154 @@ public: // Test register, release, isRegistered, and auto-register for prepared statements @("autoRegistration") debug(MYSQLN_TESTS) -unittest +@system unittest { - import mysql.connection; - import mysql.test.common; - import mysql.safe.prepared; - import mysql.safe.commands; - - Prepared preparedInsert; - Prepared preparedSelect; - immutable insertSQL = "INSERT INTO `autoRegistration` VALUES (1), (2)"; - immutable selectSQL = "SELECT `val` FROM `autoRegistration`"; - int queryTupleResult; - + static void test(bool doSafe)() { - mixin(scopedCn); + import mysql.test.common; + static if(doSafe) + { + import mysql.safe.connection; + import mysql.safe.prepared; + import mysql.safe.commands; + } + else + { + import mysql.unsafe.connection; + import mysql.unsafe.prepared; + import mysql.unsafe.commands; + } - // Setup - cn.exec("DROP TABLE IF EXISTS `autoRegistration`"); - cn.exec("CREATE TABLE `autoRegistration` ( - `val` INTEGER - ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + Prepared preparedInsert; + Prepared preparedSelect; + immutable insertSQL = "INSERT INTO `autoRegistration` VALUES (1), (2)"; + immutable selectSQL = "SELECT `val` FROM `autoRegistration`"; + int queryTupleResult; - // Initial register - preparedInsert = cn.prepare(insertSQL); - preparedSelect = cn.prepare(selectSQL); - - // Test basic register, release, isRegistered - assert(cn.isRegistered(preparedInsert)); - assert(cn.isRegistered(preparedSelect)); - cn.release(preparedInsert); - cn.release(preparedSelect); - assert(!cn.isRegistered(preparedInsert)); - assert(!cn.isRegistered(preparedSelect)); - - // Test manual re-register - cn.register(preparedInsert); - cn.register(preparedSelect); - assert(cn.isRegistered(preparedInsert)); - assert(cn.isRegistered(preparedSelect)); - - // Test double register - cn.register(preparedInsert); - cn.register(preparedSelect); - assert(cn.isRegistered(preparedInsert)); - assert(cn.isRegistered(preparedSelect)); - - // Test double release - cn.release(preparedInsert); - cn.release(preparedSelect); - assert(!cn.isRegistered(preparedInsert)); - assert(!cn.isRegistered(preparedSelect)); - cn.release(preparedInsert); - cn.release(preparedSelect); - assert(!cn.isRegistered(preparedInsert)); - assert(!cn.isRegistered(preparedSelect)); - } + { + mixin(scopedCn); + + // Setup + cn.exec("DROP TABLE IF EXISTS `autoRegistration`"); + cn.exec("CREATE TABLE `autoRegistration` ( + `val` INTEGER + ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + + // Initial register + preparedInsert = cn.prepare(insertSQL); + preparedSelect = cn.prepare(selectSQL); + + // Test basic register, release, isRegistered + assert(cn.isRegistered(preparedInsert)); + assert(cn.isRegistered(preparedSelect)); + cn.release(preparedInsert); + cn.release(preparedSelect); + assert(!cn.isRegistered(preparedInsert)); + assert(!cn.isRegistered(preparedSelect)); + + // Test manual re-register + cn.register(preparedInsert); + cn.register(preparedSelect); + assert(cn.isRegistered(preparedInsert)); + assert(cn.isRegistered(preparedSelect)); + + // Test double register + cn.register(preparedInsert); + cn.register(preparedSelect); + assert(cn.isRegistered(preparedInsert)); + assert(cn.isRegistered(preparedSelect)); + + // Test double release + cn.release(preparedInsert); + cn.release(preparedSelect); + assert(!cn.isRegistered(preparedInsert)); + assert(!cn.isRegistered(preparedSelect)); + cn.release(preparedInsert); + cn.release(preparedSelect); + assert(!cn.isRegistered(preparedInsert)); + assert(!cn.isRegistered(preparedSelect)); + } - // Note that at this point, both prepared statements still exist, - // but are no longer registered on any connection. In fact, there - // are no open connections anymore. + // Note that at this point, both prepared statements still exist, + // but are no longer registered on any connection. In fact, there + // are no open connections anymore. - // Test auto-register: exec - { - mixin(scopedCn); + // Test auto-register: exec + { + mixin(scopedCn); - assert(!cn.isRegistered(preparedInsert)); - cn.exec(preparedInsert); - assert(cn.isRegistered(preparedInsert)); - } + assert(!cn.isRegistered(preparedInsert)); + cn.exec(preparedInsert); + assert(cn.isRegistered(preparedInsert)); + } - // Test auto-register: query - { - mixin(scopedCn); + // Test auto-register: query + { + mixin(scopedCn); - assert(!cn.isRegistered(preparedSelect)); - cn.query(preparedSelect).each(); - assert(cn.isRegistered(preparedSelect)); - } + assert(!cn.isRegistered(preparedSelect)); + cn.query(preparedSelect).each(); + assert(cn.isRegistered(preparedSelect)); + } - // Test auto-register: queryRow - { - mixin(scopedCn); + // Test auto-register: queryRow + { + mixin(scopedCn); - assert(!cn.isRegistered(preparedSelect)); - cn.queryRow(preparedSelect); - assert(cn.isRegistered(preparedSelect)); - } + assert(!cn.isRegistered(preparedSelect)); + cn.queryRow(preparedSelect); + assert(cn.isRegistered(preparedSelect)); + } - // Test auto-register: queryRowTuple - { - mixin(scopedCn); + // Test auto-register: queryRowTuple + { + mixin(scopedCn); - assert(!cn.isRegistered(preparedSelect)); - cn.queryRowTuple(preparedSelect, queryTupleResult); - assert(cn.isRegistered(preparedSelect)); - } + assert(!cn.isRegistered(preparedSelect)); + cn.queryRowTuple(preparedSelect, queryTupleResult); + assert(cn.isRegistered(preparedSelect)); + } - // Test auto-register: queryValue - { - mixin(scopedCn); + // Test auto-register: queryValue + { + mixin(scopedCn); - assert(!cn.isRegistered(preparedSelect)); - cn.queryValue(preparedSelect); - assert(cn.isRegistered(preparedSelect)); + assert(!cn.isRegistered(preparedSelect)); + cn.queryValue(preparedSelect); + assert(cn.isRegistered(preparedSelect)); + } } + test!false(); + () @safe {test!true(); } (); } // An attempt to reproduce issue #81: Using mysql-native driver with no default database // I'm unable to actually reproduce the error, though. @("issue81") debug(MYSQLN_TESTS) -unittest +@system unittest { - import mysql.escape; - import mysql.safe.commands; - mixin(scopedCn); + static void test(bool doSafe)() + { + import mysql.escape; + static if(doSafe) + import mysql.safe.commands; + else + import mysql.unsafe.commands; - cn.exec("DROP TABLE IF EXISTS `issue81`"); - cn.exec("CREATE TABLE `issue81` (a INTEGER) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - cn.exec("INSERT INTO `issue81` (a) VALUES (1)"); + mixin(scopedCn); - auto cn2 = new Connection(text("host=", cn._host, ";port=", cn._port, ";user=", cn._user, ";pwd=", cn._pwd)); - scope(exit) cn2.close(); + cn.exec("DROP TABLE IF EXISTS `issue81`"); + cn.exec("CREATE TABLE `issue81` (a INTEGER) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + cn.exec("INSERT INTO `issue81` (a) VALUES (1)"); - cn2.query("SELECT * FROM `"~mysqlEscape(cn._db).text~"`.`issue81`"); + auto cn2 = new Connection(text("host=", cn._host, ";port=", cn._port, ";user=", cn._user, ";pwd=", cn._pwd)); + scope(exit) cn2.close(); + + cn2.query("SELECT * FROM `"~mysqlEscape(cn._db).text~"`.`issue81`"); + } + test!false(); + () @safe {test!true(); } (); } // Regression test for Issue #154: @@ -1110,26 +1170,40 @@ unittest // object itself. @("dropConnection") debug(MYSQLN_TESTS) -unittest +@system unittest { - import mysql.safe.commands; - import mysql.safe.connection; - mixin(scopedCn); - - cn.exec("DROP TABLE IF EXISTS `dropConnection`"); - cn.exec("CREATE TABLE `dropConnection` ( - `val` INTEGER - ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - cn.exec("INSERT INTO `dropConnection` VALUES (1), (2), (3)"); - import mysql.prepared; + static void test(bool doSafe)() { - auto prep = cn.prepare("SELECT * FROM `dropConnection`"); - cn.query(prep); + static if(doSafe) + { + import mysql.safe.commands; + import mysql.safe.connection; + } + else + { + import mysql.unsafe.commands; + import mysql.unsafe.connection; + } + mixin(scopedCn); + + cn.exec("DROP TABLE IF EXISTS `dropConnection`"); + cn.exec("CREATE TABLE `dropConnection` ( + `val` INTEGER + ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + cn.exec("INSERT INTO `dropConnection` VALUES (1), (2), (3)"); + import mysql.safe.prepared; + { + auto prep = cn.prepare("SELECT * FROM `dropConnection`"); + cn.query(prep); + } + // close the socket forcibly + cn._socket.close(); + // this should still work (it should reconnect). + cn.exec("DROP TABLE `dropConnection`"); } - // close the socket forcibly - cn._socket.close(); - // this should still work (it should reconnect). - cn.exec("DROP TABLE `dropConnection`"); + + test!false(); + () @safe {test!true(); } (); } /* diff --git a/source/mysql/impl/pool.d b/source/mysql/impl/pool.d index c170b7ea..d68a0d76 100644 --- a/source/mysql/impl/pool.d +++ b/source/mysql/impl/pool.d @@ -269,10 +269,16 @@ version(IncludeMySQLPool) unittest { auto count = 0; - void callback(Connection conn) - { - count++; - } + static if(isSafe) + @safe void callback(Connection conn) + { + count++; + } + else + @system void callback(Connection conn) + { + count++; + } // Test getting/setting auto poolA = new MySQLPoolImpl(testConnectionStr, &callback); @@ -352,6 +358,12 @@ version(IncludeMySQLPool) autoRegister(prepared.sql); } + ///ditto + void autoRegister(UnsafePrepared prepared) @safe + { + autoRegister(prepared.sql); + } + ///ditto void autoRegister(const(char[]) sql) @safe { @@ -384,6 +396,12 @@ version(IncludeMySQLPool) autoRelease(prepared.sql); } + ///ditto + void autoRelease(UnsafePrepared prepared) @safe + { + autoRelease(prepared.sql); + } + ///ditto void autoRelease(const(char[]) sql) @safe { @@ -397,6 +415,11 @@ version(IncludeMySQLPool) return isAutoRegistered(prepared.sql); } ///ditto + bool isAutoRegistered(UnsafePrepared prepared) @safe + { + return isAutoRegistered(prepared.sql); + } + ///ditto bool isAutoRegistered(const(char[]) sql) @safe { return isAutoRegistered(preparedRegistrations[sql]); @@ -414,6 +437,11 @@ version(IncludeMySQLPool) return isAutoReleased(prepared.sql); } ///ditto + bool isAutoReleased(UnsafePrepared prepared) @safe + { + return isAutoReleased(prepared.sql); + } + ///ditto bool isAutoReleased(const(char[]) sql) @safe { return isAutoReleased(preparedRegistrations[sql]); @@ -459,6 +487,11 @@ version(IncludeMySQLPool) return clearAuto(prepared.sql); } ///ditto + void clearAuto(UnsafePrepared prepared) @safe + { + return clearAuto(prepared.sql); + } + ///ditto void clearAuto(const(char[]) sql) @safe { preparedRegistrations.directLookup.remove(sql); @@ -503,18 +536,9 @@ version(IncludeMySQLPool) debug(MYSQLN_TESTS) unittest { - static void doit(bool isSafe)() + static void test(bool isSafe)() { - static if(isSafe) - { - import mysql.safe.commands; - import mysql.safe.connection; - } - else - { - import mysql.unsafe.commands; - import mysql.unsafe.connection; - } + mixin(doImports(isSafe, "commands", "connection")); alias MySQLPool = MySQLPoolImpl!isSafe; auto pool = new MySQLPool(testConnectionStr); @@ -600,20 +624,17 @@ version(IncludeMySQLPool) } // run tests for both safe and unsafe options. - () @safe {doit!true(); }(); - doit!false(); + () @safe {test!true(); }(); + test!false(); } @("closedConnection") // "cct" debug(MYSQLN_TESTS) unittest { - static void doit(bool isSafe)() + static void test(bool isSafe)() { - static if(isSafe) - import mysql.safe.commands; - else - import mysql.unsafe.commands; + mixin(doImports(isSafe, "commands")); alias MySQLPool = MySQLPoolImpl!isSafe; MySQLPool cctPool; int cctCount=0; @@ -645,7 +666,7 @@ version(IncludeMySQLPool) } // run tests for both safe and unsafe options. - () @safe {doit!true(); }(); - doit!false(); + () @safe {test!true(); }(); + test!false(); } } diff --git a/source/mysql/impl/prepared.d b/source/mysql/impl/prepared.d index 07f9c17c..99605994 100644 --- a/source/mysql/impl/prepared.d +++ b/source/mysql/impl/prepared.d @@ -63,98 +63,103 @@ alias UPSN = UnsafeParameterSpecialization; @("paramSpecial") debug(MYSQLN_TESTS) -unittest +@system unittest { - import std.array; - import std.range; - import mysql.safe.connection; - import mysql.safe.commands; - import mysql.test.common; - mixin(scopedCn); - - // Setup - cn.exec("DROP TABLE IF EXISTS `paramSpecial`"); - cn.exec("CREATE TABLE `paramSpecial` ( - `data` LONGBLOB - ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - - immutable totalSize = 1000; // Deliberately not a multiple of chunkSize below - auto alph = cast(const(ubyte)[]) "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - auto data = alph.cycle.take(totalSize).array; - - int chunkSize; - const(ubyte)[] dataToSend; - bool finished; - uint sender(ubyte[] chunk) + static void test(bool isSafe)() { - assert(!finished); - assert(chunk.length == chunkSize); + import std.array; + import std.range; + import mysql.test.common; + mixin(doImports(isSafe, "connection", "commands", "prepared")); + mixin(scopedCn); - if(dataToSend.length < chunkSize) - { - auto actualSize = cast(uint) dataToSend.length; - chunk[0..actualSize] = dataToSend[]; - finished = true; - dataToSend.length = 0; - return actualSize; - } - else + // Setup + cn.exec("DROP TABLE IF EXISTS `paramSpecial`"); + cn.exec("CREATE TABLE `paramSpecial` ( + `data` LONGBLOB + ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + + immutable totalSize = 1000; // Deliberately not a multiple of chunkSize below + auto alph = cast(const(ubyte)[]) "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + auto data = alph.cycle.take(totalSize).array; + + int chunkSize; + const(ubyte)[] dataToSend; + bool finished; + uint sender(ubyte[] chunk) { - chunk[] = dataToSend[0..chunkSize]; - dataToSend = dataToSend[chunkSize..$]; - return chunkSize; + assert(!finished); + assert(chunk.length == chunkSize); + + if(dataToSend.length < chunkSize) + { + auto actualSize = cast(uint) dataToSend.length; + chunk[0..actualSize] = dataToSend[]; + finished = true; + dataToSend.length = 0; + return actualSize; + } + else + { + chunk[] = dataToSend[0..chunkSize]; + dataToSend = dataToSend[chunkSize..$]; + return chunkSize; + } } - } - - immutable selectSQL = "SELECT `data` FROM `paramSpecial`"; - // Sanity check - cn.exec("INSERT INTO `paramSpecial` VALUES (\""~(cast(string)data)~"\")"); - auto value = cn.queryValue(selectSQL); - assert(!value.isNull); - assert(value.get == data); + immutable selectSQL = "SELECT `data` FROM `paramSpecial`"; - { - // Clear table - cn.exec("DELETE FROM `paramSpecial`"); - value = cn.queryValue(selectSQL); // Ensure deleted - assert(value.isNull); - - // Test: totalSize as a multiple of chunkSize - chunkSize = 100; - assert(cast(int)(totalSize / chunkSize) * chunkSize == totalSize); - auto paramSpecial = SafeParameterSpecialization(0, SQLType.INFER_FROM_D_TYPE, chunkSize, &sender); - - finished = false; - dataToSend = data; - auto prepared = cn.prepare("INSERT INTO `paramSpecial` VALUES (?)"); - prepared.setArg(0, cast(ubyte[])[], paramSpecial); - assert(cn.exec(prepared) == 1); - value = cn.queryValue(selectSQL); + // Sanity check + cn.exec("INSERT INTO `paramSpecial` VALUES (\""~(cast(const(char)[])data)~"\")"); + auto value = cn.queryValue(selectSQL); assert(!value.isNull); assert(value.get == data); - } - { - // Clear table - cn.exec("DELETE FROM `paramSpecial`"); - value = cn.queryValue(selectSQL); // Ensure deleted - assert(value.isNull); - - // Test: totalSize as a non-multiple of chunkSize - chunkSize = 64; - assert(cast(int)(totalSize / chunkSize) * chunkSize != totalSize); - auto paramSpecial = SafeParameterSpecialization(0, SQLType.INFER_FROM_D_TYPE, chunkSize, &sender); + { + // Clear table + cn.exec("DELETE FROM `paramSpecial`"); + value = cn.queryValue(selectSQL); // Ensure deleted + assert(value.isNull); + + // Test: totalSize as a multiple of chunkSize + chunkSize = 100; + assert(cast(int)(totalSize / chunkSize) * chunkSize == totalSize); + auto paramSpecial = ParameterSpecialization(0, SQLType.INFER_FROM_D_TYPE, chunkSize, &sender); + + finished = false; + dataToSend = data; + auto prepared = cn.prepare("INSERT INTO `paramSpecial` VALUES (?)"); + prepared.setArg(0, cast(ubyte[])[], paramSpecial); + assert(cn.exec(prepared) == 1); + value = cn.queryValue(selectSQL); + assert(!value.isNull); + assert(value.get == data); + } - finished = false; - dataToSend = data; - auto prepared = cn.prepare("INSERT INTO `paramSpecial` VALUES (?)"); - prepared.setArg(0, cast(ubyte[])[], paramSpecial); - assert(cn.exec(prepared) == 1); - value = cn.queryValue(selectSQL); - assert(!value.isNull); - assert(value.get == data); + { + // Clear table + cn.exec("DELETE FROM `paramSpecial`"); + value = cn.queryValue(selectSQL); // Ensure deleted + assert(value.isNull); + + // Test: totalSize as a non-multiple of chunkSize + chunkSize = 64; + assert(cast(int)(totalSize / chunkSize) * chunkSize != totalSize); + auto paramSpecial = ParameterSpecialization(0, SQLType.INFER_FROM_D_TYPE, chunkSize, &sender); + + finished = false; + dataToSend = data; + auto prepared = cn.prepare("INSERT INTO `paramSpecial` VALUES (?)"); + prepared.setArg(0, cast(ubyte[])[], paramSpecial); + assert(cn.exec(prepared) == 1); + value = cn.queryValue(selectSQL); + assert(!value.isNull); + assert(value.get == data); + } } + + test!false(); + () @safe {test!true();} (); } /++ @@ -266,51 +271,57 @@ public: @("setArg-typeMods") debug(MYSQLN_TESTS) - unittest + @system unittest { - import mysql.safe.commands; - import mysql.test.common; - mixin(scopedCn); - - // Setup - cn.exec("DROP TABLE IF EXISTS `setArg-typeMods`"); - cn.exec("CREATE TABLE `setArg-typeMods` ( - `i` INTEGER - ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - - auto insertSQL = "INSERT INTO `setArg-typeMods` VALUES (?)"; - - // Sanity check - { - int i = 111; - assert(cn.exec(insertSQL, i) == 1); - auto value = cn.queryValue("SELECT `i` FROM `setArg-typeMods`"); - assert(!value.isNull); - assert(value.get == i); - } - - // Test const(int) - { - const(int) i = 112; - assert(cn.exec(insertSQL, i) == 1); - } - - // Test immutable(int) + void test(bool isSafe)() { - immutable(int) i = 113; + import mysql.test.common; + mixin(doImports(isSafe, "commands")); + mixin(scopedCn); + + // Setup + cn.exec("DROP TABLE IF EXISTS `setArg-typeMods`"); + cn.exec("CREATE TABLE `setArg-typeMods` ( + `i` INTEGER + ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + + auto insertSQL = "INSERT INTO `setArg-typeMods` VALUES (?)"; + + // Sanity check + { + int i = 111; + assert(cn.exec(insertSQL, i) == 1); + auto value = cn.queryValue("SELECT `i` FROM `setArg-typeMods`"); + assert(!value.isNull); + assert(value.get == i); + } + + // Test const(int) + { + const(int) i = 112; + assert(cn.exec(insertSQL, i) == 1); + } + + // Test immutable(int) + { + immutable(int) i = 113; + assert(cn.exec(insertSQL, i) == 1); + } + + // Note: Variant doesn't seem to support + // `shared(T)` or `shared(const(T)`. Only `shared(immutable(T))`. + + // Further note, shared immutable(int) is really + // immutable(int). This test is a duplicate, so removed. + // Test shared immutable(int) + /*{ + shared immutable(int) i = 113; assert(cn.exec(insertSQL, i) == 1); + }*/ } - // Note: Variant doesn't seem to support - // `shared(T)` or `shared(const(T)`. Only `shared(immutable(T))`. - - // Further note, shared immutable(int) is really - // immutable(int). This test is a duplicate, so removed. - // Test shared immutable(int) - /*{ - shared immutable(int) i = 113; - assert(cn.exec(insertSQL, i) == 1); - }*/ + test!false(); + () @safe {test!true();} (); } /++ @@ -414,62 +425,64 @@ public: debug(MYSQLN_TESTS) unittest { - import mysql.safe.connection; - import mysql.test.common; - import mysql.safe.commands; - mixin(scopedCn); - - cn.exec("DROP TABLE IF EXISTS `setNullArg`"); - cn.exec("CREATE TABLE `setNullArg` ( - `val` INTEGER - ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - - immutable insertSQL = "INSERT INTO `setNullArg` VALUES (?)"; - immutable selectSQL = "SELECT * FROM `setNullArg`"; - auto preparedInsert = cn.prepare(insertSQL); - assert(preparedInsert.sql == insertSQL); - SafeRow[] rs; - + static void test(bool isSafe)() { - Nullable!int nullableInt; - nullableInt.nullify(); - preparedInsert.setArg(0, nullableInt); - assert(preparedInsert.getArg(0).kind == MySQLVal.Kind.Null); - nullableInt = 7; - preparedInsert.setArg(0, nullableInt); - assert(preparedInsert.getArg(0) == 7); - - nullableInt.nullify(); - preparedInsert.setArgs(nullableInt); - assert(preparedInsert.getArg(0).kind == MySQLVal.Kind.Null); - nullableInt = 7; - preparedInsert.setArgs(nullableInt); - assert(preparedInsert.getArg(0) == 7); + import mysql.test.common; + mixin(doImports(isSafe, "connection", "commands")); + mixin(scopedCn); + + cn.exec("DROP TABLE IF EXISTS `setNullArg`"); + cn.exec("CREATE TABLE `setNullArg` ( + `val` INTEGER + ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + + immutable insertSQL = "INSERT INTO `setNullArg` VALUES (?)"; + immutable selectSQL = "SELECT * FROM `setNullArg`"; + auto preparedInsert = cn.prepare(insertSQL); + assert(preparedInsert.sql == insertSQL); + SafeRow[] rs; + + { + Nullable!int nullableInt; + nullableInt.nullify(); + preparedInsert.setArg(0, nullableInt); + assert(preparedInsert.getArg(0).kind == MySQLVal.Kind.Null); + nullableInt = 7; + preparedInsert.setArg(0, nullableInt); + assert(preparedInsert.getArg(0) == 7); + + nullableInt.nullify(); + preparedInsert.setArgs(nullableInt); + assert(preparedInsert.getArg(0).kind == MySQLVal.Kind.Null); + nullableInt = 7; + preparedInsert.setArgs(nullableInt); + assert(preparedInsert.getArg(0) == 7); + } + + preparedInsert.setArg(0, 5); + cn.exec(preparedInsert); + rs = cn.query(selectSQL).array; + assert(rs.length == 1); + assert(rs[0][0] == 5); + + preparedInsert.setArg(0, null); + cn.exec(preparedInsert); + rs = cn.query(selectSQL).array; + assert(rs.length == 2); + assert(rs[0][0] == 5); + assert(rs[1].isNull(0)); + assert(rs[1][0].kind == MySQLVal.Kind.Null); + + preparedInsert.setArg(0, MySQLVal(null)); + cn.exec(preparedInsert); + rs = cn.query(selectSQL).array; + assert(rs.length == 3); + assert(rs[0][0] == 5); + assert(rs[1].isNull(0)); + assert(rs[2].isNull(0)); + assert(rs[1][0].kind == MySQLVal.Kind.Null); + assert(rs[2][0] == null); } - - preparedInsert.setArg(0, 5); - cn.exec(preparedInsert); - rs = cn.query(selectSQL).array; - assert(rs.length == 1); - assert(rs[0][0] == 5); - - preparedInsert.setArg(0, null); - cn.exec(preparedInsert); - rs = cn.query(selectSQL).array; - assert(rs.length == 2); - assert(rs[0][0] == 5); - assert(rs[1].isNull(0)); - assert(rs[1][0].kind == MySQLVal.Kind.Null); - - preparedInsert.setArg(0, MySQLVal(null)); - cn.exec(preparedInsert); - rs = cn.query(selectSQL).array; - assert(rs.length == 3); - assert(rs[0][0] == 5); - assert(rs[1].isNull(0)); - assert(rs[2].isNull(0)); - assert(rs[1][0].kind == MySQLVal.Kind.Null); - assert(rs[2][0] == null); } /// Gets the number of arguments this prepared statement expects to be passed in. @@ -485,24 +498,29 @@ public: @("lastInsertID") debug(MYSQLN_TESTS) - unittest + @system unittest { - import mysql.connection; - import mysql.safe.commands; - mixin(scopedCn); - cn.exec("DROP TABLE IF EXISTS `testPreparedLastInsertID`"); - cn.exec("CREATE TABLE `testPreparedLastInsertID` ( - `a` INTEGER NOT NULL AUTO_INCREMENT, - PRIMARY KEY (a) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + static void test(bool isSafe)() + { + mixin(doImports(isSafe, "connection", "commands")); + mixin(scopedCn); + cn.exec("DROP TABLE IF EXISTS `testPreparedLastInsertID`"); + cn.exec("CREATE TABLE `testPreparedLastInsertID` ( + `a` INTEGER NOT NULL AUTO_INCREMENT, + PRIMARY KEY (a) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + + auto stmt = cn.prepare("INSERT INTO `testPreparedLastInsertID` VALUES()"); + cn.exec(stmt); + assert(stmt.lastInsertID == 1); + cn.exec(stmt); + assert(stmt.lastInsertID == 2); + cn.exec(stmt); + assert(stmt.lastInsertID == 3); + } - auto stmt = cn.prepare("INSERT INTO `testPreparedLastInsertID` VALUES()"); - cn.exec(stmt); - assert(stmt.lastInsertID == 1); - cn.exec(stmt); - assert(stmt.lastInsertID == 2); - cn.exec(stmt); - assert(stmt.lastInsertID == 3); + test!false(); + () @safe { test!true(); }(); } /// Gets the prepared header's field descriptions. @@ -573,7 +591,7 @@ struct UnsafePrepared /// ditto void setArgs(Variant[] args, UnsafeParameterSpecialization[] psnList=null) @system { - enforce!MYX(args.length == _numParams, "Param count supplied does not match prepared statement"); + enforce!MYX(args.length == _safe._numParams, "Param count supplied does not match prepared statement"); foreach(i, ref arg; args) _safe.setArg(i, _toVal(arg)); if (psnList !is null) @@ -603,8 +621,65 @@ struct UnsafePrepared return _safe; } - /// Forward all other calls to the safe accessor - alias safe this; + // this package method is to skip the ckeck for parameter specializations + // with chunk delegates. It can only be used when using the safe prepared + // statement for execution. + package(mysql) ref SafePrepared safeForExec() @system + { + return _safe; + } + + /// forward all the methods from the safe struct. See `SafePrepared` for + /// details. + deprecated("Please use setArg(index, null)") + void setNullArg(size_t index) @safe + { + _safe.setArg(index, null); + } + + @safe pure @property: + + /// ditto + const(char)[] sql() const + { + return _safe.sql; + } + + /// ditto + ushort numArgs() const nothrow + { + return _safe.numArgs; + } + + /// ditto + ulong lastInsertID() const nothrow + { + return _safe.lastInsertID; + } + /// ditto + FieldDescription[] preparedFieldDescriptions() + { + return _safe.preparedFieldDescriptions; + } + + /// ditto + ParamDescription[] preparedParamDescriptions() + { + return _safe.preparedParamDescriptions; + } + + /// ditto + ColumnSpecialization[] columnSpecials() + { + return _safe.columnSpecials; + } + + ///ditto + void columnSpecials(ColumnSpecialization[] csa) + { + _safe.columnSpecials(csa); + } + } /// Allow conversion to UnsafePrepared from SafePrepared. diff --git a/source/mysql/impl/result.d b/source/mysql/impl/result.d index 01f11dd4..299e0d3c 100644 --- a/source/mysql/impl/result.d +++ b/source/mysql/impl/result.d @@ -97,27 +97,33 @@ public: @("getName") debug(MYSQLN_TESTS) - unittest + @system unittest { - import mysql.test.common; - import mysql.safe.commands; - mixin(scopedCn); - cn.exec("DROP TABLE IF EXISTS `row_getName`"); - cn.exec("CREATE TABLE `row_getName` (someValue INTEGER, another INTEGER) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - cn.exec("INSERT INTO `row_getName` VALUES (1, 2), (3, 4)"); - - enum sql = "SELECT another, someValue FROM `row_getName`"; - - auto rows = cn.query(sql).array; - assert(rows.length == 2); - assert(rows[0][0] == 2); - assert(rows[0][1] == 1); - assert(rows[0].getName(0) == "another"); - assert(rows[0].getName(1) == "someValue"); - assert(rows[1][0] == 4); - assert(rows[1][1] == 3); - assert(rows[1].getName(0) == "another"); - assert(rows[1].getName(1) == "someValue"); + static void test(bool isSafe)() + { + import mysql.test.common; + mixin(doImports(isSafe, "commands")); + mixin(scopedCn); + cn.exec("DROP TABLE IF EXISTS `row_getName`"); + cn.exec("CREATE TABLE `row_getName` (someValue INTEGER, another INTEGER) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + cn.exec("INSERT INTO `row_getName` VALUES (1, 2), (3, 4)"); + + enum sql = "SELECT another, someValue FROM `row_getName`"; + + auto rows = cn.query(sql).array; + assert(rows.length == 2); + assert(rows[0][0] == 2); + assert(rows[0][1] == 1); + assert(rows[0].getName(0) == "another"); + assert(rows[0].getName(1) == "someValue"); + assert(rows[1][0] == 4); + assert(rows[1][1] == 3); + assert(rows[1].getName(0) == "another"); + assert(rows[1].getName(1) == "someValue"); + } + + test!false(); + () @safe { test!true(); } (); } /++ diff --git a/source/mysql/safe/commands.d b/source/mysql/safe/commands.d index 195bed7c..1a515c3d 100644 --- a/source/mysql/safe/commands.d +++ b/source/mysql/safe/commands.d @@ -544,7 +544,7 @@ void queryRowTuple(T...)(Connection conn, ref Prepared prepared, ref T args) } /// Common implementation for `queryRowTuple` overloads. -package void queryRowTupleImpl(T...)(Connection conn, ExecQueryImplInfo info, ref T args) +package(mysql) void queryRowTupleImpl(T...)(Connection conn, ExecQueryImplInfo info, ref T args) { ulong ra; enforce!MYXNoResultRecieved(execQueryImpl(conn, info, ra)); diff --git a/source/mysql/safe/connection.d b/source/mysql/safe/connection.d index 0f091ca7..d099c8c1 100644 --- a/source/mysql/safe/connection.d +++ b/source/mysql/safe/connection.d @@ -116,7 +116,7 @@ unittest import mysql.test.integration; import std.array; mixin(scopedCn); - initBaseTestTables(cn); + initBaseTestTables!true(cn); exec(cn, `DROP PROCEDURE IF EXISTS insert2`); exec(cn, ` diff --git a/source/mysql/test/common.d b/source/mysql/test/common.d index 09b7b974..2b33239f 100644 --- a/source/mysql/test/common.d +++ b/source/mysql/test/common.d @@ -18,7 +18,7 @@ import std.traits; import std.variant; import mysql.safe.commands; -import mysql.connection; +import mysql.safe.connection; import mysql.exceptions; import mysql.protocol.extra_types; import mysql.protocol.sockets; @@ -135,4 +135,16 @@ version(DoCoreTests) return DateTime(year, month, day, hour, minute, second); } + + // generate safe or unsafe imports for unittests. + string doImports(bool isSafe, string[] imports...) + { + string result; + string subpackage = isSafe ? "safe" : "unsafe"; + foreach(im; imports) + { + result ~= "import mysql." ~ subpackage ~ "." ~ im ~ ";"; + } + return result; + } } diff --git a/source/mysql/test/integration.d b/source/mysql/test/integration.d index 1b6ab009..fd3d3f82 100644 --- a/source/mysql/test/integration.d +++ b/source/mysql/test/integration.d @@ -13,17 +13,15 @@ import std.traits; import std.typecons; import std.variant; -import mysql.safe.commands; -import mysql.safe.connection; +import mysql.safe.connection : Connection; import mysql.exceptions; import mysql.metadata; import mysql.protocol.constants; import mysql.protocol.extra_types; import mysql.protocol.packets; import mysql.protocol.sockets; -import mysql.safe.result; import mysql.test.common; -@safe: +import mysql.types; alias indexOf = std.string.indexOf; // Needed on DMD 2.064.2 @@ -32,7 +30,7 @@ debug(MYSQLN_CORE_TESTS) version = DoCoreTests; @("connect") version(DoCoreTests) -unittest +@safe unittest { import std.stdio; writeln("Basic connect test..."); @@ -42,7 +40,7 @@ unittest } debug(MYSQLN_TESTS) -unittest +@safe unittest { mixin(scopedCn); @@ -76,8 +74,9 @@ unittest debug(MYSQLN_TESTS) { - void initBaseTestTables(Connection cn) + void initBaseTestTables(bool isSafe)(Connection cn) { + mixin(doImports(isSafe, "commands")); cn.exec("DROP TABLE IF EXISTS `basetest`"); cn.exec( "CREATE TABLE `basetest` ( @@ -113,454 +112,473 @@ debug(MYSQLN_TESTS) @("basetest") debug(MYSQLN_TESTS) -unittest +@system unittest { - import mysql.safe.prepared; - - struct X + static void test(bool isSafe)() { - int a, b, c; - string s; - double d; - } - bool ok = true; + mixin(doImports(isSafe, "prepared", "commands", "connection")); - mixin(scopedCn); - initBaseTestTables(cn); - - cn.exec("delete from basetest"); - - cn.exec("insert into basetest values(" ~ - "1, -128, 255, -32768, 65535, 42, 4294967295, -9223372036854775808, 18446744073709551615, 'ABC', " ~ - "'The quick brown fox', 0x000102030405060708090a0b0c0d0e0f, '2007-01-01', " ~ - "'12:12:12', '2007-01-01 12:12:12', 1.234567890987654, 22.4, NULL, 11234.4325)"); - - auto rs = cn.query("select bytecol from basetest limit 1").array; - assert(rs.length == 1); - assert(rs[0][0] == -128); - rs = cn.query("select ubytecol from basetest limit 1").array; - assert(rs.length == 1); - assert(rs.front[0] == 255); - rs = cn.query("select shortcol from basetest limit 1").array; - assert(rs.length == 1); - assert(rs[0][0] == short.min); - rs = cn.query("select ushortcol from basetest limit 1").array; - assert(rs.length == 1); - assert(rs[0][0] == ushort.max); - rs = cn.query("select intcol from basetest limit 1").array; - assert(rs.length == 1); - assert(rs[0][0] == 42); - rs = cn.query("select uintcol from basetest limit 1").array; - assert(rs.length == 1); - assert(rs[0][0] == uint.max); - rs = cn.query("select longcol from basetest limit 1").array; - assert(rs.length == 1); - assert(rs[0][0] == long.min); - rs = cn.query("select ulongcol from basetest limit 1").array; - assert(rs.length == 1); - assert(rs[0][0] == ulong.max); - rs = cn.query("select charscol from basetest limit 1").array; - assert(rs.length == 1); - assert(rs[0][0].toString() == "ABC"); - rs = cn.query("select stringcol from basetest limit 1").array; - assert(rs.length == 1); - assert(rs[0][0].toString() == "The quick brown fox"); - rs = cn.query("select bytescol from basetest limit 1").array; - assert(rs.length == 1); - assert(rs[0][0].toString() == "[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]"); - rs = cn.query("select datecol from basetest limit 1").array; - assert(rs.length == 1); - Date d = rs[0][0].get!(Date); - assert(d.year == 2007 && d.month == 1 && d.day == 1); - rs = cn.query("select timecol from basetest limit 1").array; - assert(rs.length == 1); - TimeOfDay t = rs[0][0].get!(TimeOfDay); - assert(t.hour == 12 && t.minute == 12 && t.second == 12); - rs = cn.query("select dtcol from basetest limit 1").array; - assert(rs.length == 1); - DateTime dt = rs[0][0].get!(DateTime); - assert(dt.year == 2007 && dt.month == 1 && dt.day == 1 && dt.hour == 12 && dt.minute == 12 && dt.second == 12); - rs = cn.query("select doublecol from basetest limit 1").array; - assert(rs.length == 1); - assert(rs[0][0].toString() == "1.23457"); - rs = cn.query("select floatcol from basetest limit 1").array; - assert(rs.length == 1); - assert(rs[0][0].toString() == "22.4"); - rs = cn.query("select decimalcol from basetest limit 1").array; - assert(rs.length == 1); - assert(rs[0][0] == "11234.4325"); - - rs = cn.query("select * from basetest limit 1").array; - assert(rs.length == 1); - assert(rs[0][0] == true); - assert(rs[0][1] == -128); - assert(rs[0][2] == 255); - assert(rs[0][3] == short.min); - assert(rs[0][4] == ushort.max); - assert(rs[0][5] == 42); - assert(rs[0][6] == uint.max); - assert(rs[0][7] == long.min); - assert(rs[0][8] == ulong.max); - assert(rs[0][9].toString() == "ABC"); - assert(rs[0][10].toString() == "The quick brown fox"); - assert(rs[0][11].toString() == "[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]"); - d = rs[0][12].get!(Date); - assert(d.year == 2007 && d.month == 1 && d.day == 1); - t = rs[0][13].get!(TimeOfDay); - assert(t.hour == 12 && t.minute == 12 && t.second == 12); - dt = rs[0][14].get!(DateTime); - assert(dt.year == 2007 && dt.month == 1 && dt.day == 1 && dt.hour == 12 && dt.minute == 12 && dt.second == 12); - assert(rs[0][15].toString() == "1.23457"); - assert(rs[0][16].toString() == "22.4"); - assert(rs[0].isNull(17) == true); - assert(rs[0][18] == "11234.4325", rs[0][18].toString()); - - rs = cn.query("select bytecol, ushortcol, intcol, charscol, floatcol from basetest limit 1").array; - X x; - rs[0].toStruct(x); - assert(x.a == -128 && x.b == 65535 && x.c == 42 && x.s == "ABC" && to!string(x.d) == "22.4"); - - auto stmt = cn.prepare("select * from basetest limit 1"); - rs = cn.query(stmt).array; - assert(rs.length == 1); - assert(rs[0][0] == true); - assert(rs[0][1] == -128); - assert(rs[0][2] == 255); - assert(rs[0][3] == short.min); - assert(rs[0][4] == ushort.max); - assert(rs[0][5] == 42); - assert(rs[0][6] == uint.max); - assert(rs[0][7] == long.min); - assert(rs[0][8] == ulong.max); - assert(rs[0][9].toString() == "ABC"); - assert(rs[0][10].toString() == "The quick brown fox"); - assert(rs[0][11].toString() == "[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]"); - d = rs[0][12].get!(Date); - assert(d.year == 2007 && d.month == 1 && d.day == 1); - t = rs[0][13].get!(TimeOfDay); - assert(t.hour == 12 && t.minute == 12 && t.second == 12); - dt = rs[0][14].get!(DateTime); - assert(dt.year == 2007 && dt.month == 1 && dt.day == 1 && dt.hour == 12 && dt.minute == 12 && dt.second == 12); - assert(rs[0][15].toString() == "1.23457"); - assert(rs[0][16].toString() == "22.4"); - assert(rs[0].isNull(17) == true); - assert(rs[0][18] == "11234.4325", rs[0][18].toString()); - - stmt = cn.prepare("insert into basetest (intcol, stringcol) values(?, ?)"); - MySQLVal[] va; - va.length = 2; - va[0] = 42; - va[1] = "The quick brown fox x"; - stmt.setArgs(va); - foreach (int i; 0..20) - { - cn.exec(stmt); - stmt.setArg(0, stmt.getArg(0) + 1); - stmt.setArg(1, stmt.getArg(1) ~ "x"); - } + struct X + { + int a, b, c; + string s; + double d; + } + bool ok = true; - stmt = cn.prepare("insert into basetest (intcol, stringcol) values(?, ?)"); - //MySQLVal[] va; - va.length = 2; - va[0] = 42; - va[1] = "The quick brown fox x"; - stmt.setArgs(va); - foreach (int i; 0..20) - { - cn.exec(stmt); + mixin(scopedCn); + initBaseTestTables!isSafe(cn); + + cn.exec("delete from basetest"); + + cn.exec("insert into basetest values(" ~ + "1, -128, 255, -32768, 65535, 42, 4294967295, -9223372036854775808, 18446744073709551615, 'ABC', " ~ + "'The quick brown fox', 0x000102030405060708090a0b0c0d0e0f, '2007-01-01', " ~ + "'12:12:12', '2007-01-01 12:12:12', 1.234567890987654, 22.4, NULL, 11234.4325)"); + + auto rs = cn.query("select bytecol from basetest limit 1").array; + assert(rs.length == 1); + assert(rs[0][0] == -128); + rs = cn.query("select ubytecol from basetest limit 1").array; + assert(rs.length == 1); + assert(rs.front[0] == 255); + rs = cn.query("select shortcol from basetest limit 1").array; + assert(rs.length == 1); + assert(rs[0][0] == short.min); + rs = cn.query("select ushortcol from basetest limit 1").array; + assert(rs.length == 1); + assert(rs[0][0] == ushort.max); + rs = cn.query("select intcol from basetest limit 1").array; + assert(rs.length == 1); + assert(rs[0][0] == 42); + rs = cn.query("select uintcol from basetest limit 1").array; + assert(rs.length == 1); + assert(rs[0][0] == uint.max); + rs = cn.query("select longcol from basetest limit 1").array; + assert(rs.length == 1); + assert(rs[0][0] == long.min); + rs = cn.query("select ulongcol from basetest limit 1").array; + assert(rs.length == 1); + assert(rs[0][0] == ulong.max); + rs = cn.query("select charscol from basetest limit 1").array; + assert(rs.length == 1); + assert(rs[0][0].toString() == "ABC"); + rs = cn.query("select stringcol from basetest limit 1").array; + assert(rs.length == 1); + assert(rs[0][0].toString() == "The quick brown fox"); + rs = cn.query("select bytescol from basetest limit 1").array; + assert(rs.length == 1); + assert(rs[0][0].toString() == "[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]"); + rs = cn.query("select datecol from basetest limit 1").array; + assert(rs.length == 1); + Date d = rs[0][0].get!(Date); + assert(d.year == 2007 && d.month == 1 && d.day == 1); + rs = cn.query("select timecol from basetest limit 1").array; + assert(rs.length == 1); + TimeOfDay t = rs[0][0].get!(TimeOfDay); + assert(t.hour == 12 && t.minute == 12 && t.second == 12); + rs = cn.query("select dtcol from basetest limit 1").array; + assert(rs.length == 1); + DateTime dt = rs[0][0].get!(DateTime); + assert(dt.year == 2007 && dt.month == 1 && dt.day == 1 && dt.hour == 12 && dt.minute == 12 && dt.second == 12); + rs = cn.query("select doublecol from basetest limit 1").array; + assert(rs.length == 1); + assert(rs[0][0].toString() == "1.23457"); + rs = cn.query("select floatcol from basetest limit 1").array; + assert(rs.length == 1); + assert(rs[0][0].toString() == "22.4"); + rs = cn.query("select decimalcol from basetest limit 1").array; + assert(rs.length == 1); + assert(rs[0][0] == "11234.4325"); + + rs = cn.query("select * from basetest limit 1").array; + assert(rs.length == 1); + assert(rs[0][0] == true); + assert(rs[0][1] == -128); + assert(rs[0][2] == 255); + assert(rs[0][3] == short.min); + assert(rs[0][4] == ushort.max); + assert(rs[0][5] == 42); + assert(rs[0][6] == uint.max); + assert(rs[0][7] == long.min); + assert(rs[0][8] == ulong.max); + assert(rs[0][9].toString() == "ABC"); + assert(rs[0][10].toString() == "The quick brown fox"); + assert(rs[0][11].toString() == "[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]"); + d = rs[0][12].get!(Date); + assert(d.year == 2007 && d.month == 1 && d.day == 1); + t = rs[0][13].get!(TimeOfDay); + assert(t.hour == 12 && t.minute == 12 && t.second == 12); + dt = rs[0][14].get!(DateTime); + assert(dt.year == 2007 && dt.month == 1 && dt.day == 1 && dt.hour == 12 && dt.minute == 12 && dt.second == 12); + assert(rs[0][15].toString() == "1.23457"); + assert(rs[0][16].toString() == "22.4"); + assert(rs[0].isNull(17) == true); + assert(rs[0][18] == "11234.4325", rs[0][18].toString()); + + rs = cn.query("select bytecol, ushortcol, intcol, charscol, floatcol from basetest limit 1").array; + X x; + rs[0].toStruct(x); + assert(x.a == -128 && x.b == 65535 && x.c == 42 && x.s == "ABC" && to!string(x.d) == "22.4"); + + auto stmt = cn.prepare("select * from basetest limit 1"); + rs = cn.query(stmt).array; + assert(rs.length == 1); + assert(rs[0][0] == true); + assert(rs[0][1] == -128); + assert(rs[0][2] == 255); + assert(rs[0][3] == short.min); + assert(rs[0][4] == ushort.max); + assert(rs[0][5] == 42); + assert(rs[0][6] == uint.max); + assert(rs[0][7] == long.min); + assert(rs[0][8] == ulong.max); + assert(rs[0][9].toString() == "ABC"); + assert(rs[0][10].toString() == "The quick brown fox"); + assert(rs[0][11].toString() == "[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]"); + d = rs[0][12].get!(Date); + assert(d.year == 2007 && d.month == 1 && d.day == 1); + t = rs[0][13].get!(TimeOfDay); + assert(t.hour == 12 && t.minute == 12 && t.second == 12); + dt = rs[0][14].get!(DateTime); + assert(dt.year == 2007 && dt.month == 1 && dt.day == 1 && dt.hour == 12 && dt.minute == 12 && dt.second == 12); + assert(rs[0][15].toString() == "1.23457"); + assert(rs[0][16].toString() == "22.4"); + assert(rs[0].isNull(17) == true); + assert(rs[0][18] == "11234.4325", rs[0][18].toString()); + + stmt = cn.prepare("insert into basetest (intcol, stringcol) values(?, ?)"); + static if(isSafe) + MySQLVal[] va; + else + Variant[] va; + va.length = 2; + va[0] = 42; + va[1] = "The quick brown fox x"; + stmt.setArgs(va); + foreach (int i; 0..20) + { + cn.exec(stmt); + stmt.setArg(0, stmt.getArg(0) + 1); + stmt.setArg(1, stmt.getArg(1) ~ "x"); + } - va[0] = stmt.getArg(0).get!int + 1; - va[1] = stmt.getArg(1).get!string ~ "x"; + stmt = cn.prepare("insert into basetest (intcol, stringcol) values(?, ?)"); + //MySQLVal[] va; + va.length = 2; + va[0] = 42; + va[1] = "The quick brown fox x"; stmt.setArgs(va); - } + foreach (int i; 0..20) + { + cn.exec(stmt); - int a; - string b; - cn.queryRowTuple("select intcol, stringcol from basetest where bytecol=-128 limit 1", a, b); - assert(a == 42 && b == "The quick brown fox"); - - stmt = cn.prepare("select intcol, stringcol from basetest where bytecol=? limit 1"); - MySQLVal[] va2; - va2.length = 1; - va2[0] = cast(byte) -128; - stmt.setArgs(va2); - a = 0; - b = ""; - cn.queryRowTuple(stmt, a, b); - assert(a == 42 && b == "The quick brown fox"); - - stmt = cn.prepare("update basetest set intcol=? where bytecol=-128"); - int referred = 555; - stmt.setArgs(referred); - cn.exec(stmt); - referred = 666; - stmt.setArgs(referred); - cn.exec(stmt); - auto referredBack = cn.queryValue("select intcol from basetest where bytecol = -128"); - assert(!referredBack.isNull); - assert(referredBack.get == 666); - - // Test execFunction() - exec(cn, `DROP FUNCTION IF EXISTS hello`); - exec(cn, ` - CREATE FUNCTION hello (s CHAR(20)) - RETURNS CHAR(50) DETERMINISTIC - RETURN CONCAT('Hello ',s,'!') - `); - - rs = query(cn, "select hello ('World')").array; - assert(rs.length == 1); - assert(rs[0][0] == "Hello World!"); - - string g = "Gorgeous"; - string reply; - - auto func = cn.prepareFunction("hello", 1); - func.setArgs(g); - auto funcResult = cn.queryValue(func); - assert(!funcResult.isNull && funcResult.get == "Hello Gorgeous!"); - g = "Hotlips"; - func.setArgs(g); - funcResult = cn.queryValue(func); - assert(!funcResult.isNull && funcResult.get == "Hello Hotlips!"); - - // Test execProcedure() - exec(cn, `DROP PROCEDURE IF EXISTS insert2`); - exec(cn, ` - CREATE PROCEDURE insert2 (IN p1 INT, IN p2 CHAR(50)) - BEGIN - INSERT INTO basetest (intcol, stringcol) VALUES(p1, p2); - END - `); - g = "inserted string 1"; - int m = 2001; - auto proc = cn.prepareProcedure("insert2", 2); - proc.setArgs(m, g); - cn.exec(proc); - - cn.queryRowTuple("select stringcol from basetest where intcol=2001", reply); - assert(reply == g); - - g = "inserted string 2"; - m = 2002; - proc.setArgs(m, g); - cn.exec(proc); - - cn.queryRowTuple("select stringcol from basetest where intcol=2002", reply); - assert(reply == g); + va[0] = stmt.getArg(0).get!int + 1; + va[1] = stmt.getArg(1).get!string ~ "x"; + stmt.setArgs(va); + } -/+ - cn.exec("delete from tblob"); - cn.exec("insert into tblob values(321, NULL, 22.4, NULL, '2011-11-05 11:52:00')"); -+/ - size_t delegate(ubyte[]) foo() - { - size_t n = 20000000; - uint cp = 0; + int a; + string b; + cn.queryRowTuple("select intcol, stringcol from basetest where bytecol=-128 limit 1", a, b); + assert(a == 42 && b == "The quick brown fox"); + + stmt = cn.prepare("select intcol, stringcol from basetest where bytecol=? limit 1"); + static if(isSafe) + MySQLVal[] va2; + else + Variant[] va2; + va2.length = 1; + va2[0] = cast(byte) -128; + stmt.setArgs(va2); + a = 0; + b = ""; + cn.queryRowTuple(stmt, a, b); + assert(a == 42 && b == "The quick brown fox"); + + stmt = cn.prepare("update basetest set intcol=? where bytecol=-128"); + int referred = 555; + stmt.setArgs(referred); + cn.exec(stmt); + referred = 666; + stmt.setArgs(referred); + cn.exec(stmt); + auto referredBack = cn.queryValue("select intcol from basetest where bytecol = -128"); + assert(!referredBack.isNull); + assert(referredBack.get == 666); + + // Test execFunction() + exec(cn, `DROP FUNCTION IF EXISTS hello`); + exec(cn, ` + CREATE FUNCTION hello (s CHAR(20)) + RETURNS CHAR(50) DETERMINISTIC + RETURN CONCAT('Hello ',s,'!') + `); + + rs = query(cn, "select hello ('World')").array; + assert(rs.length == 1); + assert(rs[0][0] == "Hello World!"); + + string g = "Gorgeous"; + string reply; + + auto func = cn.prepareFunction("hello", 1); + func.setArgs(g); + auto funcResult = cn.queryValue(func); + assert(!funcResult.isNull && funcResult.get == "Hello Gorgeous!"); + g = "Hotlips"; + func.setArgs(g); + funcResult = cn.queryValue(func); + assert(!funcResult.isNull && funcResult.get == "Hello Hotlips!"); + + // Test execProcedure() + exec(cn, `DROP PROCEDURE IF EXISTS insert2`); + exec(cn, ` + CREATE PROCEDURE insert2 (IN p1 INT, IN p2 CHAR(50)) + BEGIN + INSERT INTO basetest (intcol, stringcol) VALUES(p1, p2); + END + `); + g = "inserted string 1"; + int m = 2001; + auto proc = cn.prepareProcedure("insert2", 2); + proc.setArgs(m, g); + cn.exec(proc); + + cn.queryRowTuple("select stringcol from basetest where intcol=2001", reply); + assert(reply == g); + + g = "inserted string 2"; + m = 2002; + proc.setArgs(m, g); + cn.exec(proc); + + cn.queryRowTuple("select stringcol from basetest where intcol=2002", reply); + assert(reply == g); - void fill(ubyte[] a, size_t m) + /+ + cn.exec("delete from tblob"); + cn.exec("insert into tblob values(321, NULL, 22.4, NULL, '2011-11-05 11:52:00')"); + +/ + size_t delegate(ubyte[]) foo() { - foreach (size_t i; 0..m) + size_t n = 20000000; + uint cp = 0; + + void fill(ubyte[] a, size_t m) { - a[i] = cast(ubyte) (cp & 0xff); - cp++; + foreach (size_t i; 0..m) + { + a[i] = cast(ubyte) (cp & 0xff); + cp++; + } } - } - size_t dg(ubyte[] dest) - { - size_t len = dest.length; - if (n >= len) + size_t dg(ubyte[] dest) { - fill(dest, len); - n -= len; - return len; + size_t len = dest.length; + if (n >= len) + { + fill(dest, len); + n -= len; + return len; + } + fill(dest, n); + return n; } - fill(dest, n); - return n; - } - - return &dg; - } -/+ - stmt = cn.prepare("update tblob set lob=?, lob2=? where ikey=321"); - ubyte[] uba; - ubyte[] uba2; - stmt.bindParameter(uba, 0, PSN(0, false, SQLType.LONGBLOB, 10000, foo())); - stmt.bindParameter(uba2, 1, PSN(1, false, SQLType.LONGBLOB, 10000, foo())); - stmt.exec(); - - uint got1, got2; - bool verified1, verified2; - void delegate(ubyte[], bool) bar1(ref uint got, ref bool verified) - { - got = 0; - verified = true; - void dg(ubyte[] ba, bool finished) + return &dg; + } + /+ + stmt = cn.prepare("update tblob set lob=?, lob2=? where ikey=321"); + ubyte[] uba; + ubyte[] uba2; + stmt.bindParameter(uba, 0, PSN(0, false, SQLType.LONGBLOB, 10000, foo())); + stmt.bindParameter(uba2, 1, PSN(1, false, SQLType.LONGBLOB, 10000, foo())); + stmt.exec(); + + uint got1, got2; + bool verified1, verified2; + void delegate(ubyte[], bool) bar1(ref uint got, ref bool verified) { - foreach (uint; 0..ba.length) + got = 0; + verified = true; + + void dg(ubyte[] ba, bool finished) { - if (verified && ba[i] != ((got+i) & 0xff)) - verified = false; + foreach (uint; 0..ba.length) + { + if (verified && ba[i] != ((got+i) & 0xff)) + verified = false; + } + got += ba.length; } - got += ba.length; + return &dg; } - return &dg; - } - - void delegate(ubyte[], bool) bar2(ref uint got, ref bool verified) - { - got = 0; - verified = true; - void dg(ubyte[] ba, bool finished) + void delegate(ubyte[], bool) bar2(ref uint got, ref bool verified) { - foreach (size_t i; 0..ba.length) + got = 0; + verified = true; + + void dg(ubyte[] ba, bool finished) { - if (verified && ba[i] != ((got+i) & 0xff)) - verified = false; + foreach (size_t i; 0..ba.length) + { + if (verified && ba[i] != ((got+i) & 0xff)) + verified = false; + } + got += ba.length; } - got += ba.length; + return &dg; } - return &dg; - } - rs = cn.query("select * from tblob limit 1"); - ubyte[] blob = rs[0][1].get!(ubyte[]); - ubyte[] blob2 = rs[0][3].get!(ubyte[]); - DateTime dt4 = rs[0][4].get!(DateTime); - writefln("blob. lengths %d %d", blob.length, blob2.length); - writeln(to!string(dt4)); + rs = cn.query("select * from tblob limit 1"); + ubyte[] blob = rs[0][1].get!(ubyte[]); + ubyte[] blob2 = rs[0][3].get!(ubyte[]); + DateTime dt4 = rs[0][4].get!(DateTime); + writefln("blob. lengths %d %d", blob.length, blob2.length); + writeln(to!string(dt4)); - CSN[] csa = [ CSN(1, 0xfc, 100000, bar1(got1, verified1)), CSN(3, 0xfc, 100000, bar2(got2, verified2)) ]; - rs = cn.query("select * from tblob limit 1", csa); - writefln("1) %d, %s", got1, verified1); - writefln("2) %d, %s", got2, verified2); - DateTime dt4 = rs[0][4].get!(DateTime); - writeln(to!string(dt4)); -+/ + CSN[] csa = [ CSN(1, 0xfc, 100000, bar1(got1, verified1)), CSN(3, 0xfc, 100000, bar2(got2, verified2)) ]; + rs = cn.query("select * from tblob limit 1", csa); + writefln("1) %d, %s", got1, verified1); + writefln("2) %d, %s", got2, verified2); + DateTime dt4 = rs[0][4].get!(DateTime); + writeln(to!string(dt4)); + +/ + } + + test!false(); + () @safe { test!true(); } (); } + @("MetaData") debug(MYSQLN_TESTS) -unittest +@system unittest { - mixin(scopedCn); - auto schemaName = cn.currentDB; - MetaData md = MetaData(cn); - string[] dbList = md.databases(); - int count = 0; - foreach (string db; dbList) + static void test(bool isSafe)() { - if (db == schemaName || db == "information_schema") - count++; - } - assert(count == 2); + mixin(scopedCn); + auto schemaName = cn.currentDB; + MetaData md = MetaData(cn); + string[] dbList = md.databases(); + int count = 0; + foreach (string db; dbList) + { + if (db == schemaName || db == "information_schema") + count++; + } + assert(count == 2); - initBaseTestTables(cn); + initBaseTestTables!isSafe(cn); - string[] tList = md.tables(); - count = 0; - foreach (string t; tList) - { - if (t == "basetest" || t == "tblob") - count++; + string[] tList = md.tables(); + count = 0; + foreach (string t; tList) + { + if (t == "basetest" || t == "tblob") + count++; + } + assert(count == 2); + + /+ + Don't check defaultNull or defaultValue here because, for columns + with a default value of NULL, their values could be different + depending on the server. + + See "COLUMN_DEFAULT" at: + https://mariadb.com/kb/en/library/information-schema-columns-table/ + +/ + + ColumnInfo[] ca = md.columns("basetest"); + assert( ca[0].schema == schemaName && ca[0].table == "basetest" && ca[0].name == "boolcol" && ca[0].index == 0 && + ca[0].nullable && ca[0].type == "bit" && ca[0].charsMax == -1 && ca[0].octetsMax == -1 && + ca[0].numericPrecision == 1 && ca[0].numericScale == -1 && ca[0].charSet == "" && ca[0].collation == "" && + ca[0].colType == "bit(1)"); + assert( ca[1].schema == schemaName && ca[1].table == "basetest" && ca[1].name == "bytecol" && ca[1].index == 1 && + ca[1].nullable && ca[1].type == "tinyint" && ca[1].charsMax == -1 && ca[1].octetsMax == -1 && + ca[1].numericPrecision == 3 && ca[1].numericScale == 0 && ca[1].charSet == "" && ca[1].collation == "" && + ca[1].colType == "tinyint(4)"); + assert( ca[2].schema == schemaName && ca[2].table == "basetest" && ca[2].name == "ubytecol" && ca[2].index == 2 && + ca[2].nullable && ca[2].type == "tinyint" && ca[2].charsMax == -1 && ca[2].octetsMax == -1 && + ca[2].numericPrecision == 3 && ca[2].numericScale == 0 && ca[2].charSet == "" && ca[2].collation == "" && + ca[2].colType == "tinyint(3) unsigned"); + assert( ca[3].schema == schemaName && ca[3].table == "basetest" && ca[3].name == "shortcol" && ca[3].index == 3 && + ca[3].nullable && ca[3].type == "smallint" && ca[3].charsMax == -1 && ca[3].octetsMax == -1 && + ca[3].numericPrecision == 5 && ca[3].numericScale == 0 && ca[3].charSet == "" && ca[3].collation == "" && + ca[3].colType == "smallint(6)"); + assert( ca[4].schema == schemaName && ca[4].table == "basetest" && ca[4].name == "ushortcol" && ca[4].index == 4 && + ca[4].nullable && ca[4].type == "smallint" && ca[4].charsMax == -1 && ca[4].octetsMax == -1 && + ca[4].numericPrecision == 5 && ca[4].numericScale == 0 && ca[4].charSet == "" && ca[4].collation == "" && + ca[4].colType == "smallint(5) unsigned"); + assert( ca[5].schema == schemaName && ca[5].table == "basetest" && ca[5].name == "intcol" && ca[5].index == 5 && + ca[5].nullable && ca[5].type == "int" && ca[5].charsMax == -1 && ca[5].octetsMax == -1 && + ca[5].numericPrecision == 10 && ca[5].numericScale == 0 && ca[5].charSet == "" && ca[5].collation == "" && + ca[5].colType == "int(11)"); + assert( ca[6].schema == schemaName && ca[6].table == "basetest" && ca[6].name == "uintcol" && ca[6].index == 6 && + ca[6].nullable && ca[6].type == "int" && ca[6].charsMax == -1 && ca[6].octetsMax == -1 && + ca[6].numericPrecision == 10 && ca[6].numericScale == 0 && ca[6].charSet == "" && ca[6].collation == "" && + ca[6].colType == "int(10) unsigned"); + assert( ca[7].schema == schemaName && ca[7].table == "basetest" && ca[7].name == "longcol" && ca[7].index == 7 && + ca[7].nullable && ca[7].type == "bigint" && ca[7].charsMax == -1 && ca[7].octetsMax == -1 && + ca[7].numericPrecision == 19 && ca[7].numericScale == 0 && ca[7].charSet == "" && ca[7].collation == "" && + ca[7].colType == "bigint(20)"); + assert( ca[8].schema == schemaName && ca[8].table == "basetest" && ca[8].name == "ulongcol" && ca[8].index == 8 && + ca[8].nullable && ca[8].type == "bigint" && ca[8].charsMax == -1 && ca[8].octetsMax == -1 && + //TODO: I'm getting numericPrecision==19, figure it out later + /+ca[8].numericPrecision == 20 &&+/ ca[8].numericScale == 0 && ca[8].charSet == "" && ca[8].collation == "" && + ca[8].colType == "bigint(20) unsigned"); + assert( ca[9].schema == schemaName && ca[9].table == "basetest" && ca[9].name == "charscol" && ca[9].index == 9 && + ca[9].nullable && ca[9].type == "char" && ca[9].charsMax == 10 && ca[9].octetsMax == 10 && + ca[9].numericPrecision == -1 && ca[9].numericScale == -1 && ca[9].charSet == "latin1" && ca[9].collation == "latin1_swedish_ci" && + ca[9].colType == "char(10)"); + assert( ca[10].schema == schemaName && ca[10].table == "basetest" && ca[10].name == "stringcol" && ca[10].index == 10 && + ca[10].nullable && ca[10].type == "varchar" && ca[10].charsMax == 50 && ca[10].octetsMax == 50 && + ca[10].numericPrecision == -1 && ca[10].numericScale == -1 && ca[10].charSet == "latin1" && ca[10].collation == "latin1_swedish_ci" && + ca[10].colType == "varchar(50)"); + assert( ca[11].schema == schemaName && ca[11].table == "basetest" && ca[11].name == "bytescol" && ca[11].index == 11 && + ca[11].nullable && ca[11].type == "tinyblob" && ca[11].charsMax == 255 && ca[11].octetsMax == 255 && + ca[11].numericPrecision == -1 && ca[11].numericScale == -1 && ca[11].charSet == "" && ca[11].collation == "" && + ca[11].colType == "tinyblob"); + assert( ca[12].schema == schemaName && ca[12].table == "basetest" && ca[12].name == "datecol" && ca[12].index == 12 && + ca[12].nullable && ca[12].type == "date" && ca[12].charsMax == -1 && ca[12].octetsMax == -1 && + ca[12].numericPrecision == -1 && ca[12].numericScale == -1 && ca[12].charSet == "" && ca[12].collation == "" && + ca[12].colType == "date"); + assert( ca[13].schema == schemaName && ca[13].table == "basetest" && ca[13].name == "timecol" && ca[13].index == 13 && + ca[13].nullable && ca[13].type == "time" && ca[13].charsMax == -1 && ca[13].octetsMax == -1 && + ca[13].numericPrecision == -1 && ca[13].numericScale == -1 && ca[13].charSet == "" && ca[13].collation == "" && + ca[13].colType == "time"); + assert( ca[14].schema == schemaName && ca[14].table == "basetest" && ca[14].name == "dtcol" && ca[14].index == 14 && + ca[14].nullable && ca[14].type == "datetime" && ca[14].charsMax == -1 && ca[14].octetsMax == -1 && + ca[14].numericPrecision == -1 && ca[14].numericScale == -1 && ca[14].charSet == "" && ca[14].collation == "" && + ca[14].colType == "datetime"); + assert( ca[15].schema == schemaName && ca[15].table == "basetest" && ca[15].name == "doublecol" && ca[15].index == 15 && + ca[15].nullable && ca[15].type == "double" && ca[15].charsMax == -1 && ca[15].octetsMax == -1 && + ca[15].numericPrecision == 22 && ca[15].numericScale == -1 && ca[15].charSet == "" && ca[15].collation == "" && + ca[15].colType == "double"); + assert( ca[16].schema == schemaName && ca[16].table == "basetest" && ca[16].name == "floatcol" && ca[16].index == 16 && + ca[16].nullable && ca[16].type == "float" && ca[16].charsMax == -1 && ca[16].octetsMax == -1 && + ca[16].numericPrecision == 12 && ca[16].numericScale == -1 && ca[16].charSet == "" && ca[16].collation == "" && + ca[16].colType == "float"); + assert( ca[17].schema == schemaName && ca[17].table == "basetest" && ca[17].name == "nullcol" && ca[17].index == 17 && + ca[17].nullable && ca[17].type == "int" && ca[17].charsMax == -1 && ca[17].octetsMax == -1 && + ca[17].numericPrecision == 10 && ca[17].numericScale == 0 && ca[17].charSet == "" && ca[17].collation == "" && + ca[17].colType == "int(11)"); + assert( ca[18].schema == schemaName && ca[18].table == "basetest" && ca[18].name == "decimalcol" && ca[18].index == 18 && + ca[18].nullable && ca[18].type == "decimal" && ca[18].charsMax == -1 && ca[18].octetsMax == -1 && + ca[18].numericPrecision == 11 && ca[18].numericScale == 4 && ca[18].charSet == "" && ca[18].collation == "" && + ca[18].colType == "decimal(11,4)"); + MySQLProcedure[] pa = md.functions(); + //assert(pa[0].db == schemaName && pa[0].name == "hello" && pa[0].type == "FUNCTION"); + //pa = md.procedures(); + //assert(pa[0].db == schemaName && pa[0].name == "insert2" && pa[0].type == "PROCEDURE"); } - assert(count == 2); - - /+ - Don't check defaultNull or defaultValue here because, for columns - with a default value of NULL, their values could be different - depending on the server. - See "COLUMN_DEFAULT" at: - https://mariadb.com/kb/en/library/information-schema-columns-table/ - +/ - - ColumnInfo[] ca = md.columns("basetest"); - assert( ca[0].schema == schemaName && ca[0].table == "basetest" && ca[0].name == "boolcol" && ca[0].index == 0 && - ca[0].nullable && ca[0].type == "bit" && ca[0].charsMax == -1 && ca[0].octetsMax == -1 && - ca[0].numericPrecision == 1 && ca[0].numericScale == -1 && ca[0].charSet == "" && ca[0].collation == "" && - ca[0].colType == "bit(1)"); - assert( ca[1].schema == schemaName && ca[1].table == "basetest" && ca[1].name == "bytecol" && ca[1].index == 1 && - ca[1].nullable && ca[1].type == "tinyint" && ca[1].charsMax == -1 && ca[1].octetsMax == -1 && - ca[1].numericPrecision == 3 && ca[1].numericScale == 0 && ca[1].charSet == "" && ca[1].collation == "" && - ca[1].colType == "tinyint(4)"); - assert( ca[2].schema == schemaName && ca[2].table == "basetest" && ca[2].name == "ubytecol" && ca[2].index == 2 && - ca[2].nullable && ca[2].type == "tinyint" && ca[2].charsMax == -1 && ca[2].octetsMax == -1 && - ca[2].numericPrecision == 3 && ca[2].numericScale == 0 && ca[2].charSet == "" && ca[2].collation == "" && - ca[2].colType == "tinyint(3) unsigned"); - assert( ca[3].schema == schemaName && ca[3].table == "basetest" && ca[3].name == "shortcol" && ca[3].index == 3 && - ca[3].nullable && ca[3].type == "smallint" && ca[3].charsMax == -1 && ca[3].octetsMax == -1 && - ca[3].numericPrecision == 5 && ca[3].numericScale == 0 && ca[3].charSet == "" && ca[3].collation == "" && - ca[3].colType == "smallint(6)"); - assert( ca[4].schema == schemaName && ca[4].table == "basetest" && ca[4].name == "ushortcol" && ca[4].index == 4 && - ca[4].nullable && ca[4].type == "smallint" && ca[4].charsMax == -1 && ca[4].octetsMax == -1 && - ca[4].numericPrecision == 5 && ca[4].numericScale == 0 && ca[4].charSet == "" && ca[4].collation == "" && - ca[4].colType == "smallint(5) unsigned"); - assert( ca[5].schema == schemaName && ca[5].table == "basetest" && ca[5].name == "intcol" && ca[5].index == 5 && - ca[5].nullable && ca[5].type == "int" && ca[5].charsMax == -1 && ca[5].octetsMax == -1 && - ca[5].numericPrecision == 10 && ca[5].numericScale == 0 && ca[5].charSet == "" && ca[5].collation == "" && - ca[5].colType == "int(11)"); - assert( ca[6].schema == schemaName && ca[6].table == "basetest" && ca[6].name == "uintcol" && ca[6].index == 6 && - ca[6].nullable && ca[6].type == "int" && ca[6].charsMax == -1 && ca[6].octetsMax == -1 && - ca[6].numericPrecision == 10 && ca[6].numericScale == 0 && ca[6].charSet == "" && ca[6].collation == "" && - ca[6].colType == "int(10) unsigned"); - assert( ca[7].schema == schemaName && ca[7].table == "basetest" && ca[7].name == "longcol" && ca[7].index == 7 && - ca[7].nullable && ca[7].type == "bigint" && ca[7].charsMax == -1 && ca[7].octetsMax == -1 && - ca[7].numericPrecision == 19 && ca[7].numericScale == 0 && ca[7].charSet == "" && ca[7].collation == "" && - ca[7].colType == "bigint(20)"); - assert( ca[8].schema == schemaName && ca[8].table == "basetest" && ca[8].name == "ulongcol" && ca[8].index == 8 && - ca[8].nullable && ca[8].type == "bigint" && ca[8].charsMax == -1 && ca[8].octetsMax == -1 && - //TODO: I'm getting numericPrecision==19, figure it out later - /+ca[8].numericPrecision == 20 &&+/ ca[8].numericScale == 0 && ca[8].charSet == "" && ca[8].collation == "" && - ca[8].colType == "bigint(20) unsigned"); - assert( ca[9].schema == schemaName && ca[9].table == "basetest" && ca[9].name == "charscol" && ca[9].index == 9 && - ca[9].nullable && ca[9].type == "char" && ca[9].charsMax == 10 && ca[9].octetsMax == 10 && - ca[9].numericPrecision == -1 && ca[9].numericScale == -1 && ca[9].charSet == "latin1" && ca[9].collation == "latin1_swedish_ci" && - ca[9].colType == "char(10)"); - assert( ca[10].schema == schemaName && ca[10].table == "basetest" && ca[10].name == "stringcol" && ca[10].index == 10 && - ca[10].nullable && ca[10].type == "varchar" && ca[10].charsMax == 50 && ca[10].octetsMax == 50 && - ca[10].numericPrecision == -1 && ca[10].numericScale == -1 && ca[10].charSet == "latin1" && ca[10].collation == "latin1_swedish_ci" && - ca[10].colType == "varchar(50)"); - assert( ca[11].schema == schemaName && ca[11].table == "basetest" && ca[11].name == "bytescol" && ca[11].index == 11 && - ca[11].nullable && ca[11].type == "tinyblob" && ca[11].charsMax == 255 && ca[11].octetsMax == 255 && - ca[11].numericPrecision == -1 && ca[11].numericScale == -1 && ca[11].charSet == "" && ca[11].collation == "" && - ca[11].colType == "tinyblob"); - assert( ca[12].schema == schemaName && ca[12].table == "basetest" && ca[12].name == "datecol" && ca[12].index == 12 && - ca[12].nullable && ca[12].type == "date" && ca[12].charsMax == -1 && ca[12].octetsMax == -1 && - ca[12].numericPrecision == -1 && ca[12].numericScale == -1 && ca[12].charSet == "" && ca[12].collation == "" && - ca[12].colType == "date"); - assert( ca[13].schema == schemaName && ca[13].table == "basetest" && ca[13].name == "timecol" && ca[13].index == 13 && - ca[13].nullable && ca[13].type == "time" && ca[13].charsMax == -1 && ca[13].octetsMax == -1 && - ca[13].numericPrecision == -1 && ca[13].numericScale == -1 && ca[13].charSet == "" && ca[13].collation == "" && - ca[13].colType == "time"); - assert( ca[14].schema == schemaName && ca[14].table == "basetest" && ca[14].name == "dtcol" && ca[14].index == 14 && - ca[14].nullable && ca[14].type == "datetime" && ca[14].charsMax == -1 && ca[14].octetsMax == -1 && - ca[14].numericPrecision == -1 && ca[14].numericScale == -1 && ca[14].charSet == "" && ca[14].collation == "" && - ca[14].colType == "datetime"); - assert( ca[15].schema == schemaName && ca[15].table == "basetest" && ca[15].name == "doublecol" && ca[15].index == 15 && - ca[15].nullable && ca[15].type == "double" && ca[15].charsMax == -1 && ca[15].octetsMax == -1 && - ca[15].numericPrecision == 22 && ca[15].numericScale == -1 && ca[15].charSet == "" && ca[15].collation == "" && - ca[15].colType == "double"); - assert( ca[16].schema == schemaName && ca[16].table == "basetest" && ca[16].name == "floatcol" && ca[16].index == 16 && - ca[16].nullable && ca[16].type == "float" && ca[16].charsMax == -1 && ca[16].octetsMax == -1 && - ca[16].numericPrecision == 12 && ca[16].numericScale == -1 && ca[16].charSet == "" && ca[16].collation == "" && - ca[16].colType == "float"); - assert( ca[17].schema == schemaName && ca[17].table == "basetest" && ca[17].name == "nullcol" && ca[17].index == 17 && - ca[17].nullable && ca[17].type == "int" && ca[17].charsMax == -1 && ca[17].octetsMax == -1 && - ca[17].numericPrecision == 10 && ca[17].numericScale == 0 && ca[17].charSet == "" && ca[17].collation == "" && - ca[17].colType == "int(11)"); - assert( ca[18].schema == schemaName && ca[18].table == "basetest" && ca[18].name == "decimalcol" && ca[18].index == 18 && - ca[18].nullable && ca[18].type == "decimal" && ca[18].charsMax == -1 && ca[18].octetsMax == -1 && - ca[18].numericPrecision == 11 && ca[18].numericScale == 4 && ca[18].charSet == "" && ca[18].collation == "" && - ca[18].colType == "decimal(11,4)"); - MySQLProcedure[] pa = md.functions(); - //assert(pa[0].db == schemaName && pa[0].name == "hello" && pa[0].type == "FUNCTION"); - //pa = md.procedures(); - //assert(pa[0].db == schemaName && pa[0].name == "insert2" && pa[0].type == "PROCEDURE"); + test!false(); + () @safe { test!true(); } (); } /+ @@ -571,124 +589,130 @@ https://github.com/simendsjo/mysqln // Bind values in prepared statements @("bind-values") debug(MYSQLN_TESTS) -unittest +@system unittest { - import mysql.safe.prepared; - mixin(scopedCn); - cn.exec("DROP TABLE IF EXISTS manytypes"); - cn.exec( "CREATE TABLE manytypes (" - ~" i INT" - ~", f FLOAT" - ~", dttm DATETIME" - ~", dt DATE" - ~")"); - - //DataSet ds; - Row[] rs; - //Table tbl; - Row row; - Prepared stmt; - - // Index out of bounds throws - /+ - try + void test(bool isSafe)() { - cn.query_("SELECT TRUE", 1); - assert(0); - } - catch(Exception ex) {} - +/ + mixin(doImports(isSafe, "result", "prepared", "connection", "commands")); + mixin(scopedCn); + cn.exec("DROP TABLE IF EXISTS manytypes"); + cn.exec( "CREATE TABLE manytypes (" + ~" i INT" + ~", f FLOAT" + ~", dttm DATETIME" + ~", dt DATE" + ~")"); + + //DataSet ds; + Row[] rs; + //Table tbl; + Row row; + Prepared stmt; + + // Index out of bounds throws + /+ + try + { + cn.query_("SELECT TRUE", 1); + assert(0); + } + catch(Exception ex) {} + +/ - // Select without result - cn.truncate("manytypes"); - cn.exec("INSERT INTO manytypes (i, f) VALUES (1, NULL)"); - stmt = cn.prepare("SELECT * FROM manytypes WHERE i = ?"); - { - auto val = 2; - stmt.setArg(0, val); - } - //ds = cn.query_(stmt); - //assert(ds.length == 1); - //assert(ds[0].length == 0); - rs = cn.query(stmt).array; - assert(rs.length == 0); - - // Bind single primitive value - cn.truncate("manytypes"); - cn.exec("INSERT INTO manytypes (i, f) VALUES (1, NULL)"); - stmt = cn.prepare("SELECT * FROM manytypes WHERE i = ?"); - { - auto val = 1; - stmt.setArg(0, val); - } - cn.queryValue(stmt); + // Select without result + cn.truncate("manytypes"); + cn.exec("INSERT INTO manytypes (i, f) VALUES (1, NULL)"); + stmt = cn.prepare("SELECT * FROM manytypes WHERE i = ?"); + { + auto val = 2; + stmt.setArg(0, val); + } + //ds = cn.query_(stmt); + //assert(ds.length == 1); + //assert(ds[0].length == 0); + rs = cn.query(stmt).array; + assert(rs.length == 0); - // Bind multiple primitive values - cn.truncate("manytypes"); - cn.exec("INSERT INTO manytypes (i, f) VALUES (1, 2)"); - { - auto val1 = 1; - auto val2 = 2; - stmt = cn.prepare("SELECT * FROM manytypes WHERE i = ? AND f = ?"); - stmt.setArgs(val1, val2); - row = cn.queryRow(stmt); - } - assert(row[0] == 1); - assert(row[1] == 2); + // Bind single primitive value + cn.truncate("manytypes"); + cn.exec("INSERT INTO manytypes (i, f) VALUES (1, NULL)"); + stmt = cn.prepare("SELECT * FROM manytypes WHERE i = ?"); + { + auto val = 1; + stmt.setArg(0, val); + } + cn.queryValue(stmt); - /+ - // Commented out because leaving args unspecified is currently unsupported, - // and I'm not convinced it should be allowed. + // Bind multiple primitive values + cn.truncate("manytypes"); + cn.exec("INSERT INTO manytypes (i, f) VALUES (1, 2)"); + { + auto val1 = 1; + auto val2 = 2; + stmt = cn.prepare("SELECT * FROM manytypes WHERE i = ? AND f = ?"); + stmt.setArgs(val1, val2); + row = cn.queryRow(stmt); + } + assert(row[0] == 1); + assert(row[1] == 2); - // Insert null - params defaults to null - { + /+ + // Commented out because leaving args unspecified is currently unsupported, + // and I'm not convinced it should be allowed. + + // Insert null - params defaults to null + { + cn.truncate("manytypes"); + auto prep = cn.prepare("INSERT INTO manytypes (i, f) VALUES (1, ?)"); + cn.exec(prep); + cn.assertScalar!int("SELECT i FROM manytypes WHERE f IS NULL", 1); + } + +/ + + // Insert null cn.truncate("manytypes"); - auto prep = cn.prepare("INSERT INTO manytypes (i, f) VALUES (1, ?)"); - cn.exec(prep); + { + auto prepared = cn.prepare("INSERT INTO manytypes (i, f) VALUES (1, ?)"); + //TODO: Using `prepared.setArgs(null);` results in: Param count supplied does not match prepared statement + // Can anything be done about that? + prepared.setArg(0, null); + cn.exec(prepared); + } cn.assertScalar!int("SELECT i FROM manytypes WHERE f IS NULL", 1); - } - +/ - // Insert null - cn.truncate("manytypes"); - { - auto prepared = cn.prepare("INSERT INTO manytypes (i, f) VALUES (1, ?)"); - //TODO: Using `prepared.setArgs(null);` results in: Param count supplied does not match prepared statement - // Can anything be done about that? - prepared.setArg(0, null); - cn.exec(prepared); - } - cn.assertScalar!int("SELECT i FROM manytypes WHERE f IS NULL", 1); + // select where null + cn.truncate("manytypes"); + cn.exec("INSERT INTO manytypes (i, f) VALUES (1, NULL)"); + { + stmt = cn.prepare("SELECT i FROM manytypes WHERE f <=> ?"); + //TODO: Using `stmt.setArgs(null);` results in: Param count supplied does not match prepared statement + // Can anything be done about that? + stmt.setArg(0, null); + auto value = cn.queryValue(stmt); + assert(!value.isNull); + assert(value.get.get!int == 1); + } - // select where null - cn.truncate("manytypes"); - cn.exec("INSERT INTO manytypes (i, f) VALUES (1, NULL)"); - { - stmt = cn.prepare("SELECT i FROM manytypes WHERE f <=> ?"); - //TODO: Using `stmt.setArgs(null);` results in: Param count supplied does not match prepared statement - // Can anything be done about that? - stmt.setArg(0, null); - auto value = cn.queryValue(stmt); - assert(!value.isNull); - assert(value.get.get!int == 1); + // rebind parameter + cn.truncate("manytypes"); + cn.exec("INSERT INTO manytypes (i, f) VALUES (1, NULL)"); + auto cmd = cn.prepare("SELECT i FROM manytypes WHERE f <=> ?"); + cmd.setArg(0, 1); + auto tbl = cn.query(cmd).array(); + assert(tbl.length == 0); + cmd.setArg(0, null); + assert(cn.queryValue(cmd).get.get!int == 1); } - // rebind parameter - cn.truncate("manytypes"); - cn.exec("INSERT INTO manytypes (i, f) VALUES (1, NULL)"); - auto cmd = cn.prepare("SELECT i FROM manytypes WHERE f <=> ?"); - cmd.setArg(0, 1); - auto tbl = cn.query(cmd).array(); - assert(tbl.length == 0); - cmd.setArg(0, null); - assert(cn.queryValue(cmd).get.get!int == 1); + test!false(); + () @safe { test!true(); }(); } // Simple commands @("simple-commands") debug(MYSQLN_TESTS) -unittest +@system unittest { mixin(scopedCn); @@ -808,20 +832,27 @@ unittest // Simple text queries @("simple-text-queries") debug(MYSQLN_TESTS) -unittest +@system unittest { - mixin(scopedCn); - auto ds = cn.query("SELECT 1").array; - assert(ds.length == 1); - //auto rs = ds[0]; - //assert(rs.rows.length == 1); - //auto row = rs.rows[0]; - auto rs = ds; - assert(rs.length == 1); - auto row = rs[0]; - //assert(row.length == 1); - assert(row._values.length == 1); - assert(row[0].get!long == 1); + static void test(bool isSafe)() + { + mixin(scopedCn); + mixin(doImports(isSafe, "commands")); + auto ds = cn.query("SELECT 1").array; + assert(ds.length == 1); + //auto rs = ds[0]; + //assert(rs.rows.length == 1); + //auto row = rs.rows[0]; + auto rs = ds; + assert(rs.length == 1); + auto row = rs[0]; + //assert(row.length == 1); + assert(row._values.length == 1); + assert(row[0].get!long == 1); + } + + test!false(); + () @safe { test!true(); }(); } /+ @@ -849,322 +880,352 @@ unittest // Create and query table @("create-and-query-table") debug(MYSQLN_TESTS) -unittest +@system unittest { - import mysql.safe.prepared; - mixin(scopedCn); - - void assertBasicTests(T, U)(string sqlType, U[] values ...) @safe + static void test(bool isSafe)() { - import std.array; - immutable tablename = "`basic_"~sqlType.replace(" ", "")~"`"; - cn.exec("CREATE TABLE IF NOT EXISTS "~tablename~" (value "~sqlType~ ")"); - - // Missing and NULL - cn.exec("TRUNCATE "~tablename); - immutable selectOneSql = "SELECT value FROM "~tablename~" LIMIT 1"; - //assert(cn.query_(selectOneSql)[0].length == 0); - assert(cn.query(selectOneSql).array.length == 0); - - immutable insertNullSql = "INSERT INTO "~tablename~" VALUES (NULL)"; - auto okp = cn.exec(insertNullSql); - //assert(okp.affectedRows == 1); - assert(okp == 1); - auto insertNullStmt = cn.prepare(insertNullSql); - okp = cn.exec(insertNullStmt); - //assert(okp.affectedRows == 1); - assert(okp == 1); - - //assert(!cn.queryScalar(selectOneSql).hasValue); - auto x = cn.queryValue(selectOneSql); - assert(!x.isNull); - assert(x.get.kind == MySQLVal.Kind.Null); - - // NULL as bound param - auto inscmd = cn.prepare("INSERT INTO "~tablename~" VALUES (?)"); - cn.exec("TRUNCATE "~tablename); - - inscmd.setArgs([MySQLVal(null)]); - okp = cn.exec(inscmd); - //assert(okp.affectedRows == 1, "value not inserted"); - assert(okp == 1, "value not inserted"); - - //assert(!cn.queryScalar(selectOneSql).hasValue); - x = cn.queryValue(selectOneSql); - assert(!x.isNull); - assert(x.get.kind == MySQLVal.Kind.Null); - - // Values - void assertBasicTestsValue(T, U)(U val) + mixin(doImports(isSafe, "prepared", "commands", "connection", "result")); + mixin(scopedCn); + + void assertBasicTests(T, U)(string sqlType, U[] values ...) { + import std.array; + immutable tablename = "`basic_"~sqlType.replace(" ", "")~"`"; + cn.exec("CREATE TABLE IF NOT EXISTS "~tablename~" (value "~sqlType~ ")"); + + // Missing and NULL + cn.exec("TRUNCATE "~tablename); + immutable selectOneSql = "SELECT value FROM "~tablename~" LIMIT 1"; + //assert(cn.query_(selectOneSql)[0].length == 0); + assert(cn.query(selectOneSql).array.length == 0); + + immutable insertNullSql = "INSERT INTO "~tablename~" VALUES (NULL)"; + auto okp = cn.exec(insertNullSql); + //assert(okp.affectedRows == 1); + assert(okp == 1); + auto insertNullStmt = cn.prepare(insertNullSql); + okp = cn.exec(insertNullStmt); + //assert(okp.affectedRows == 1); + assert(okp == 1); + + //assert(!cn.queryScalar(selectOneSql).hasValue); + auto x = cn.queryValue(selectOneSql); + assert(!x.isNull); + static if(isSafe) + assert(x.get.kind == MySQLVal.Kind.Null); + else + assert(x.get.type == typeid(null)); + + // NULL as bound param + auto inscmd = cn.prepare("INSERT INTO "~tablename~" VALUES (?)"); cn.exec("TRUNCATE "~tablename); - inscmd.setArg(0, val); - auto ra = cn.exec(inscmd); - assert(ra == 1, "value not inserted"); + static if(isSafe) + inscmd.setArgs([MySQLVal(null)]); + else + inscmd.setArgs([Variant(null)]); + okp = cn.exec(inscmd); + //assert(okp.affectedRows == 1, "value not inserted"); + assert(okp == 1, "value not inserted"); + + //assert(!cn.queryScalar(selectOneSql).hasValue); + x = cn.queryValue(selectOneSql); + assert(!x.isNull); + static if(isSafe) + assert(x.get.kind == MySQLVal.Kind.Null); + else + assert(x.get.type == typeid(null)); + + // Values + void assertBasicTestsValue(T, U)(U val) + { + cn.exec("TRUNCATE "~tablename); - cn.assertScalar!(Unqual!T)(selectOneSql, val); - } - foreach(value; values) - { - assertBasicTestsValue!(T)(value); - assertBasicTestsValue!(T)(cast(const(U))value); - assertBasicTestsValue!(T)((() @trusted => cast(immutable(U))value)()); - // Note, shared(immutable(U)) is equivalent to immutable(U), so we - // are avoiding doing that test 2x. - static assert(is(shared(immutable(U)) == immutable(U))); - //assertBasicTestsValue!(T)(cast(shared(immutable(U)))value); + inscmd.setArg(0, val); + auto ra = cn.exec(inscmd); + assert(ra == 1, "value not inserted"); + + cn.assertScalar!(Unqual!T)(selectOneSql, val); + } + foreach(value; values) + { + assertBasicTestsValue!(T)(value); + assertBasicTestsValue!(T)(cast(const(U))value); + assertBasicTestsValue!(T)((() @trusted => cast(immutable(U))value)()); + // Note, shared(immutable(U)) is equivalent to immutable(U), so we + // are avoiding doing that test 2x. + static assert(is(shared(immutable(U)) == immutable(U))); + //assertBasicTestsValue!(T)(cast(shared(immutable(U)))value); + } } + + // TODO: Add tests for epsilon + assertBasicTests!float("FLOAT", 0.0f, 0.1f, -0.1f, 1.0f, -1.0f); + assertBasicTests!double("DOUBLE", 0.0, 0.1, -0.1, 1.0, -1.0); + + // TODO: Why don't these work? + //assertBasicTests!bool("BOOL", true, false); + //assertBasicTests!bool("TINYINT(1)", true, false); + assertBasicTests!byte("BOOl", cast(byte)1, cast(byte)0); + assertBasicTests!byte("TINYINT(1)", cast(byte)1, cast(byte)0); + + assertBasicTests!byte("TINYINT", + cast(byte)0, cast(byte)1, cast(byte)-1, byte.min, byte.max); + assertBasicTests!ubyte("TINYINT UNSIGNED", + cast(ubyte)0, cast(ubyte)1, ubyte.max); + assertBasicTests!short("SMALLINT", + cast(short)0, cast(short)1, cast(short)-1, short.min, short.max); + assertBasicTests!ushort("SMALLINT UNSIGNED", + cast(ushort)0, cast(ushort)1, ushort.max); + assertBasicTests!int("INT", 0, 1, -1, int.min, int.max); + assertBasicTests!uint("INT UNSIGNED", 0U, 1U, uint.max); + assertBasicTests!long("BIGINT", 0L, 1L, -1L, long.min, long.max); + assertBasicTests!ulong("BIGINT UNSIGNED", 0LU, 1LU, ulong.max); + + assertBasicTests!string("VARCHAR(10)", "", "aoeu"); + assertBasicTests!string("CHAR(10)", "", "aoeu"); + + assertBasicTests!string("TINYTEXT", "", "aoeu"); + assertBasicTests!string("MEDIUMTEXT", "", "aoeu"); + assertBasicTests!string("TEXT", "", "aoeu"); + assertBasicTests!string("LONGTEXT", "", "aoeu"); + + assertBasicTests!(ubyte[])("TINYBLOB", "", "aoeu"); + assertBasicTests!(ubyte[])("MEDIUMBLOB", "", "aoeu"); + assertBasicTests!(ubyte[])("BLOB", "", "aoeu"); + assertBasicTests!(ubyte[])("LONGBLOB", "", "aoeu"); + + assertBasicTests!(ubyte[])("TINYBLOB", cast(ubyte[])"".dup, cast(ubyte[])"aoeu".dup); + assertBasicTests!(ubyte[])("TINYBLOB", "".dup, "aoeu".dup); + + assertBasicTests!Date("DATE", Date(2013, 10, 03)); + assertBasicTests!DateTime("DATETIME", DateTime(2013, 10, 03, 12, 55, 35)); + assertBasicTests!TimeOfDay("TIME", TimeOfDay(12, 55, 35)); + //assertBasicTests!DateTime("TIMESTAMP NULL", Timestamp(2013_10_03_12_55_35)); + //TODO: Add Timestamp } - // TODO: Add tests for epsilon - assertBasicTests!float("FLOAT", 0.0f, 0.1f, -0.1f, 1.0f, -1.0f); - assertBasicTests!double("DOUBLE", 0.0, 0.1, -0.1, 1.0, -1.0); - - // TODO: Why don't these work? - //assertBasicTests!bool("BOOL", true, false); - //assertBasicTests!bool("TINYINT(1)", true, false); - assertBasicTests!byte("BOOl", cast(byte)1, cast(byte)0); - assertBasicTests!byte("TINYINT(1)", cast(byte)1, cast(byte)0); - - assertBasicTests!byte("TINYINT", - cast(byte)0, cast(byte)1, cast(byte)-1, byte.min, byte.max); - assertBasicTests!ubyte("TINYINT UNSIGNED", - cast(ubyte)0, cast(ubyte)1, ubyte.max); - assertBasicTests!short("SMALLINT", - cast(short)0, cast(short)1, cast(short)-1, short.min, short.max); - assertBasicTests!ushort("SMALLINT UNSIGNED", - cast(ushort)0, cast(ushort)1, ushort.max); - assertBasicTests!int("INT", 0, 1, -1, int.min, int.max); - assertBasicTests!uint("INT UNSIGNED", 0U, 1U, uint.max); - assertBasicTests!long("BIGINT", 0L, 1L, -1L, long.min, long.max); - assertBasicTests!ulong("BIGINT UNSIGNED", 0LU, 1LU, ulong.max); - - assertBasicTests!string("VARCHAR(10)", "", "aoeu"); - assertBasicTests!string("CHAR(10)", "", "aoeu"); - - assertBasicTests!string("TINYTEXT", "", "aoeu"); - assertBasicTests!string("MEDIUMTEXT", "", "aoeu"); - assertBasicTests!string("TEXT", "", "aoeu"); - assertBasicTests!string("LONGTEXT", "", "aoeu"); - - assertBasicTests!(ubyte[])("TINYBLOB", "", "aoeu"); - assertBasicTests!(ubyte[])("MEDIUMBLOB", "", "aoeu"); - assertBasicTests!(ubyte[])("BLOB", "", "aoeu"); - assertBasicTests!(ubyte[])("LONGBLOB", "", "aoeu"); - - assertBasicTests!(ubyte[])("TINYBLOB", cast(ubyte[])"".dup, cast(ubyte[])"aoeu".dup); - assertBasicTests!(ubyte[])("TINYBLOB", "".dup, "aoeu".dup); - - assertBasicTests!Date("DATE", Date(2013, 10, 03)); - assertBasicTests!DateTime("DATETIME", DateTime(2013, 10, 03, 12, 55, 35)); - assertBasicTests!TimeOfDay("TIME", TimeOfDay(12, 55, 35)); - //assertBasicTests!DateTime("TIMESTAMP NULL", Timestamp(2013_10_03_12_55_35)); - //TODO: Add Timestamp + test!false(); + () @safe { test!true(); }(); } @("info_character_sets") debug(MYSQLN_TESTS) -unittest +@system unittest { - import mysql.safe.prepared; - mixin(scopedCn); - auto stmt = cn.prepare( - "SELECT * FROM information_schema.character_sets"~ - " WHERE CHARACTER_SET_NAME=?"); - auto val = "utf8"; - stmt.setArg(0, val); - auto row = cn.queryRow(stmt).get(Row.init); - //assert(row.length == 4); - assert(row.length == 4); - assert(row[0] == "utf8"); - assert(row[1] == "utf8_general_ci"); - assert(row[2] == "UTF-8 Unicode"); - assert(row[3] == 3); + static void test(bool isSafe)() + { + mixin(doImports(isSafe, "commands", "connection", "result")); + mixin(scopedCn); + auto stmt = cn.prepare( + "SELECT * FROM information_schema.character_sets"~ + " WHERE CHARACTER_SET_NAME=?"); + auto val = "utf8"; + stmt.setArg(0, val); + auto row = cn.queryRow(stmt).get(Row.init); + //assert(row.length == 4); + assert(row.length == 4); + assert(row[0] == "utf8"); + assert(row[1] == "utf8_general_ci"); + assert(row[2] == "UTF-8 Unicode"); + assert(row[3] == 3); + } + + test!false(); + () @safe { test!true(); }(); } @("coupleTypes") debug(MYSQLN_TESTS) -unittest +@system unittest { - import mysql.safe.prepared; - mixin(scopedCn); + static void test(bool isSafe)() + { + mixin(doImports(isSafe, "prepared", "commands", "connection", "result")); + mixin(scopedCn); - cn.exec("DROP TABLE IF EXISTS `coupleTypes`"); - cn.exec("CREATE TABLE `coupleTypes` ( - `i` INTEGER, - `s` VARCHAR(50) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - cn.exec("INSERT INTO `coupleTypes` VALUES (11, 'aaa'), (22, 'bbb'), (33, 'ccc')"); + cn.exec("DROP TABLE IF EXISTS `coupleTypes`"); + cn.exec("CREATE TABLE `coupleTypes` ( + `i` INTEGER, + `s` VARCHAR(50) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + cn.exec("INSERT INTO `coupleTypes` VALUES (11, 'aaa'), (22, 'bbb'), (33, 'ccc')"); - immutable selectSQL = "SELECT * FROM `coupleTypes` ORDER BY i ASC"; - immutable selectBackwardsSQL = "SELECT `s`,`i` FROM `coupleTypes` ORDER BY i DESC"; - immutable selectNoRowsSQL = "SELECT * FROM `coupleTypes` WHERE s='no such match'"; - auto prepared = cn.prepare(selectSQL); - auto preparedSelectNoRows = cn.prepare(selectNoRowsSQL); + immutable selectSQL = "SELECT * FROM `coupleTypes` ORDER BY i ASC"; + immutable selectBackwardsSQL = "SELECT `s`,`i` FROM `coupleTypes` ORDER BY i DESC"; + immutable selectNoRowsSQL = "SELECT * FROM `coupleTypes` WHERE s='no such match'"; + auto prepared = cn.prepare(selectSQL); + auto preparedSelectNoRows = cn.prepare(selectNoRowsSQL); - { - // Test query - ResultRange rseq = cn.query(selectSQL); - assert(!rseq.empty); - assert(rseq.front.length == 2); - assert(rseq.front[0] == 11); - assert(rseq.front[1] == "aaa"); - rseq.popFront(); - assert(!rseq.empty); - assert(rseq.front.length == 2); - assert(rseq.front[0] == 22); - assert(rseq.front[1] == "bbb"); - rseq.popFront(); - assert(!rseq.empty); - assert(rseq.front.length == 2); - assert(rseq.front[0] == 33); - assert(rseq.front[1] == "ccc"); - rseq.popFront(); - assert(rseq.empty); - } + { + // Test query + ResultRange rseq = cn.query(selectSQL); + assert(!rseq.empty); + assert(rseq.front.length == 2); + assert(rseq.front[0] == 11); + assert(rseq.front[1] == "aaa"); + rseq.popFront(); + assert(!rseq.empty); + assert(rseq.front.length == 2); + assert(rseq.front[0] == 22); + assert(rseq.front[1] == "bbb"); + rseq.popFront(); + assert(!rseq.empty); + assert(rseq.front.length == 2); + assert(rseq.front[0] == 33); + assert(rseq.front[1] == "ccc"); + rseq.popFront(); + assert(rseq.empty); + } - { - // Test prepared query - ResultRange rseq = cn.query(prepared); - assert(!rseq.empty); - assert(rseq.front.length == 2); - assert(rseq.front[0] == 11); - assert(rseq.front[1] == "aaa"); - rseq.popFront(); - assert(!rseq.empty); - assert(rseq.front.length == 2); - assert(rseq.front[0] == 22); - assert(rseq.front[1] == "bbb"); - rseq.popFront(); - assert(!rseq.empty); - assert(rseq.front.length == 2); - assert(rseq.front[0] == 33); - assert(rseq.front[1] == "ccc"); - rseq.popFront(); - assert(rseq.empty); - } + { + // Test prepared query + ResultRange rseq = cn.query(prepared); + assert(!rseq.empty); + assert(rseq.front.length == 2); + assert(rseq.front[0] == 11); + assert(rseq.front[1] == "aaa"); + rseq.popFront(); + assert(!rseq.empty); + assert(rseq.front.length == 2); + assert(rseq.front[0] == 22); + assert(rseq.front[1] == "bbb"); + rseq.popFront(); + assert(!rseq.empty); + assert(rseq.front.length == 2); + assert(rseq.front[0] == 33); + assert(rseq.front[1] == "ccc"); + rseq.popFront(); + assert(rseq.empty); + } - { - // Test reusing the same ResultRange - ResultRange rseq = cn.query(selectSQL); - assert(!rseq.empty); - rseq.each(); - assert(rseq.empty); - rseq = cn.query(selectSQL); - //assert(!rseq.empty); //TODO: Why does this fail??? - rseq.each(); - assert(rseq.empty); - } + { + // Test reusing the same ResultRange + ResultRange rseq = cn.query(selectSQL); + assert(!rseq.empty); + rseq.each(); + assert(rseq.empty); + rseq = cn.query(selectSQL); + //assert(!rseq.empty); //TODO: Why does this fail??? + rseq.each(); + assert(rseq.empty); + } - { - Nullable!Row nullableRow; - - // Test queryRow - nullableRow = cn.queryRow(selectSQL); - assert(!nullableRow.isNull); - assert(nullableRow[0] == 11); - assert(nullableRow[1] == "aaa"); - // Were all results correctly purged? Can I still issue another command? - cn.query(selectSQL).array; - - nullableRow = cn.queryRow(selectNoRowsSQL); - assert(nullableRow.isNull); - - // Test prepared queryRow - nullableRow = cn.queryRow(prepared); - assert(!nullableRow.isNull); - assert(nullableRow[0] == 11); - assert(nullableRow[1] == "aaa"); - // Were all results correctly purged? Can I still issue another command? - cn.query(selectSQL).array; - - nullableRow = cn.queryRow(preparedSelectNoRows); - assert(nullableRow.isNull); - } + { + Nullable!Row nullableRow; + + // Test queryRow + nullableRow = cn.queryRow(selectSQL); + assert(!nullableRow.isNull); + assert(nullableRow[0] == 11); + assert(nullableRow[1] == "aaa"); + // Were all results correctly purged? Can I still issue another command? + cn.query(selectSQL).array; + + nullableRow = cn.queryRow(selectNoRowsSQL); + assert(nullableRow.isNull); + + // Test prepared queryRow + nullableRow = cn.queryRow(prepared); + assert(!nullableRow.isNull); + assert(nullableRow[0] == 11); + assert(nullableRow[1] == "aaa"); + // Were all results correctly purged? Can I still issue another command? + cn.query(selectSQL).array; + + nullableRow = cn.queryRow(preparedSelectNoRows); + assert(nullableRow.isNull); + } - { - int resultI; - string resultS; - - // Test queryRowTuple - cn.queryRowTuple(selectSQL, resultI, resultS); - assert(resultI == 11); - assert(resultS == "aaa"); - // Were all results correctly purged? Can I still issue another command? - cn.query(selectSQL).array; - - // Test prepared queryRowTuple - cn.queryRowTuple(prepared, resultI, resultS); - assert(resultI == 11); - assert(resultS == "aaa"); - // Were all results correctly purged? Can I still issue another command? - cn.query(selectSQL).array; - } + { + int resultI; + string resultS; + + // Test queryRowTuple + cn.queryRowTuple(selectSQL, resultI, resultS); + assert(resultI == 11); + assert(resultS == "aaa"); + // Were all results correctly purged? Can I still issue another command? + cn.query(selectSQL).array; + + // Test prepared queryRowTuple + cn.queryRowTuple(prepared, resultI, resultS); + assert(resultI == 11); + assert(resultS == "aaa"); + // Were all results correctly purged? Can I still issue another command? + cn.query(selectSQL).array; + } - { - Nullable!MySQLVal result; - - // Test queryValue - result = cn.queryValue(selectSQL); - assert(!result.isNull); - assert(result.get == 11); // Explicit "get" here works around DMD #17482 - // Were all results correctly purged? Can I still issue another command? - cn.query(selectSQL).array; - - result = cn.queryValue(selectNoRowsSQL); - assert(result.isNull); - - // Test prepared queryValue - result = cn.queryValue(prepared); - assert(!result.isNull); - assert(result.get == 11); // Explicit "get" here works around DMD #17482 - // Were all results correctly purged? Can I still issue another command? - cn.query(selectSQL).array; - - result = cn.queryValue(preparedSelectNoRows); - assert(result.isNull); - } + { + static if(isSafe) + Nullable!MySQLVal result; + else + Nullable!Variant result; + + // Test queryValue + result = cn.queryValue(selectSQL); + assert(!result.isNull); + assert(result.get == 11); // Explicit "get" here works around DMD #17482 + // Were all results correctly purged? Can I still issue another command? + cn.query(selectSQL).array; + + result = cn.queryValue(selectNoRowsSQL); + assert(result.isNull); + + // Test prepared queryValue + result = cn.queryValue(prepared); + assert(!result.isNull); + assert(result.get == 11); // Explicit "get" here works around DMD #17482 + // Were all results correctly purged? Can I still issue another command? + cn.query(selectSQL).array; + + result = cn.queryValue(preparedSelectNoRows); + assert(result.isNull); + } - { - // Issue new command before old command was purged - // Ensure old result set is auto-purged and invalidated. - ResultRange rseq1 = cn.query(selectSQL); - rseq1.popFront(); - assert(!rseq1.empty); - assert(rseq1.isValid); - assert(rseq1.front[0] == 22); - - cn.query(selectBackwardsSQL); - assert(rseq1.empty); - assert(!rseq1.isValid); - } + { + // Issue new command before old command was purged + // Ensure old result set is auto-purged and invalidated. + ResultRange rseq1 = cn.query(selectSQL); + rseq1.popFront(); + assert(!rseq1.empty); + assert(rseq1.isValid); + assert(rseq1.front[0] == 22); + + cn.query(selectBackwardsSQL); + assert(rseq1.empty); + assert(!rseq1.isValid); + } - { - // Test using outdated ResultRange - ResultRange rseq1 = cn.query(selectSQL); - rseq1.popFront(); - assert(!rseq1.empty); - assert(rseq1.front[0] == 22); - - cn.purgeResult(); - - assert(rseq1.empty); - assertThrown!MYXInvalidatedRange(rseq1.front); - assertThrown!MYXInvalidatedRange(rseq1.popFront()); - assertThrown!MYXInvalidatedRange(rseq1.asAA()); - - ResultRange rseq2 = cn.query(selectBackwardsSQL); - assert(!rseq2.empty); - assert(rseq2.front.length == 2); - assert(rseq2.front[0] == "ccc"); - assert(rseq2.front[1] == 33); - - assert(rseq1.empty); - assertThrown!MYXInvalidatedRange(rseq1.front); - assertThrown!MYXInvalidatedRange(rseq1.popFront()); - assertThrown!MYXInvalidatedRange(rseq1.asAA()); + { + // Test using outdated ResultRange + ResultRange rseq1 = cn.query(selectSQL); + rseq1.popFront(); + assert(!rseq1.empty); + assert(rseq1.front[0] == 22); + + cn.purgeResult(); + + assert(rseq1.empty); + assertThrown!MYXInvalidatedRange(rseq1.front); + assertThrown!MYXInvalidatedRange(rseq1.popFront()); + assertThrown!MYXInvalidatedRange(rseq1.asAA()); + + ResultRange rseq2 = cn.query(selectBackwardsSQL); + assert(!rseq2.empty); + assert(rseq2.front.length == 2); + assert(rseq2.front[0] == "ccc"); + assert(rseq2.front[1] == 33); + + assert(rseq1.empty); + assertThrown!MYXInvalidatedRange(rseq1.front); + assertThrown!MYXInvalidatedRange(rseq1.popFront()); + assertThrown!MYXInvalidatedRange(rseq1.asAA()); + } } + + test!false(); + () @safe { test!true(); }(); } // Test example.d @@ -1183,6 +1244,7 @@ unittest // Setup DB for test { mixin(scopedCn); + import mysql.safe.commands; cn.exec("DROP TABLE IF EXISTS `tablename`"); cn.exec("CREATE TABLE `tablename` ( diff --git a/source/mysql/test/regression.d b/source/mysql/test/regression.d index e5bf24ac..5176a756 100644 --- a/source/mysql/test/regression.d +++ b/source/mysql/test/regression.d @@ -19,174 +19,223 @@ import std.string; import std.traits; import std.variant; -import mysql.safe.commands; -import mysql.connection; import mysql.exceptions; -import mysql.prepared; import mysql.protocol.sockets; -import mysql.safe.result; import mysql.test.common; // Issue #24: Driver doesn't like BIT @("issue24") debug(MYSQLN_TESTS) -unittest +@system unittest { - mixin(scopedCn); - ulong rowsAffected; - cn.exec("DROP TABLE IF EXISTS `issue24`"); - cn.exec( - "CREATE TABLE `issue24` ( - `bit` BIT, - `date` DATE - ) ENGINE=InnoDB DEFAULT CHARSET=utf8" - ); - - cn.exec("INSERT INTO `issue24` (`bit`, `date`) VALUES (1, '1970-01-01')"); - cn.exec("INSERT INTO `issue24` (`bit`, `date`) VALUES (0, '1950-04-24')"); - - auto stmt = cn.prepare("SELECT `bit`, `date` FROM `issue24` ORDER BY `date` DESC"); - auto results = cn.query(stmt).array; - assert(results.length == 2); - assert(results[0][0] == true); - assert(results[0][1] == Date(1970, 1, 1)); - assert(results[1][0] == false); - assert(results[1][1] == Date(1950, 4, 24)); + static void test(bool isSafe)() + { + mixin(doImports(isSafe, "connection", "commands")); + mixin(scopedCn); + ulong rowsAffected; + cn.exec("DROP TABLE IF EXISTS `issue24`"); + cn.exec( + "CREATE TABLE `issue24` ( + `bit` BIT, + `date` DATE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8" + ); + + cn.exec("INSERT INTO `issue24` (`bit`, `date`) VALUES (1, '1970-01-01')"); + cn.exec("INSERT INTO `issue24` (`bit`, `date`) VALUES (0, '1950-04-24')"); + + auto stmt = cn.prepare("SELECT `bit`, `date` FROM `issue24` ORDER BY `date` DESC"); + auto results = cn.query(stmt).array; + assert(results.length == 2); + assert(results[0][0] == true); + assert(results[0][1] == Date(1970, 1, 1)); + assert(results[1][0] == false); + assert(results[1][1] == Date(1950, 4, 24)); + } + + test!false(); + () @safe { test!true(); }(); } // Issue #28: MySQLProtocolException thrown when using large integers as prepared parameters. @("issue28") debug(MYSQLN_TESTS) -unittest +@system unittest { - mixin(scopedCn); - - cn.exec("DROP TABLE IF EXISTS `issue28`"); - cn.exec("CREATE TABLE IF NOT EXISTS `issue28` ( - `added` DATETIME NOT NULL - ) ENGINE = InnoDB DEFAULT CHARACTER SET = utf8 COLLATE = utf8_bin"); - cn.exec("INSERT INTO `issue28` (added) VALUES (NOW())"); - - auto prepared = cn.prepare( - "SELECT added - FROM `issue28` WHERE UNIX_TIMESTAMP(added) >= (? - ?)"); - - uint baseTimeStamp = 1371477821; - uint cacheCutOffLimit = int.max; - - prepared.setArgs(baseTimeStamp, cacheCutOffLimit); - auto e = collectException( cn.query(prepared).array ); - assert(e !is null); - auto myxReceived = cast(MYXReceived) e; - assert(myxReceived !is null); + static void test(bool isSafe)() + { + mixin(scopedCn); + mixin(doImports(isSafe, "commands", "connection")); + + cn.exec("DROP TABLE IF EXISTS `issue28`"); + cn.exec("CREATE TABLE IF NOT EXISTS `issue28` ( + `added` DATETIME NOT NULL + ) ENGINE = InnoDB DEFAULT CHARACTER SET = utf8 COLLATE = utf8_bin"); + cn.exec("INSERT INTO `issue28` (added) VALUES (NOW())"); + + auto prepared = cn.prepare( + "SELECT added + FROM `issue28` WHERE UNIX_TIMESTAMP(added) >= (? - ?)"); + + uint baseTimeStamp = 1371477821; + uint cacheCutOffLimit = int.max; + + prepared.setArgs(baseTimeStamp, cacheCutOffLimit); + auto e = collectException( cn.query(prepared).array ); + assert(e !is null); + auto myxReceived = cast(MYXReceived) e; + assert(myxReceived !is null); + } + + test!false(); + () @safe { test!true(); }(); } // Issue #33: TINYTEXT, TEXT, MEDIUMTEXT, LONGTEXT types treated as ubyte[] @("issue33") debug(MYSQLN_TESTS) -unittest +@system unittest { - mixin(scopedCn); - cn.exec("DROP TABLE IF EXISTS `issue33`"); - cn.exec( - "CREATE TABLE `issue33` ( - `text` TEXT, - `blob` BLOB - ) ENGINE=InnoDB DEFAULT CHARSET=utf8" - ); - - cn.exec("INSERT INTO `issue33` (`text`, `blob`) VALUES ('hello', 'world')"); - - auto stmt = cn.prepare("SELECT `text`, `blob` FROM `issue33`"); - auto results = cn.query(stmt).array; - assert(results.length == 1); - auto pText = results[0][0].peek!string(); - auto pBlob = results[0][1].peek!(ubyte[])(); - assert(pText); - assert(pBlob); - assert(*pText == "hello"); - assert(*pBlob == cast(ubyte[])"world".dup); + static void test(bool isSafe)() + { + mixin(scopedCn); + mixin(doImports(isSafe, "connection", "commands")); + import mysql.types; + cn.exec("DROP TABLE IF EXISTS `issue33`"); + cn.exec( + "CREATE TABLE `issue33` ( + `text` TEXT, + `blob` BLOB + ) ENGINE=InnoDB DEFAULT CHARSET=utf8" + ); + + cn.exec("INSERT INTO `issue33` (`text`, `blob`) VALUES ('hello', 'world')"); + + auto stmt = cn.prepare("SELECT `text`, `blob` FROM `issue33`"); + auto results = cn.query(stmt).array; + assert(results.length == 1); + auto pText = results[0][0].peek!string(); + auto pBlob = results[0][1].peek!(ubyte[])(); + assert(pText); + assert(pBlob); + assert(*pText == "hello"); + assert(*pBlob == cast(ubyte[])"world".dup); + } + + test!false(); + // Note: peek is @system, so we can't run the safe version of this test. + // Perhaps when dip1000 is the default, we can make this work. + //() @safe { test!true(); }(); } // Issue #39: Unsupported SQL type NEWDECIMAL @("issue39-NEWDECIMAL") debug(MYSQLN_TESTS) -unittest +@system unittest { - mixin(scopedCn); - auto rows = cn.query("SELECT SUM(123.456)").array; - assert(rows.length == 1); - assert(rows[0][0] == "123.456"); + static void test(bool isSafe)() + { + mixin(scopedCn); + mixin(doImports(isSafe, "commands")); + auto rows = cn.query("SELECT SUM(123.456)").array; + assert(rows.length == 1); + assert(rows[0][0] == "123.456"); + } + + test!false(); + () @safe { test!true(); }(); } // Issue #40: Decoding LCB value for large feilds // And likely Issue #18: select varchar - thinks the package is incomplete while it's actually complete @("issue40") debug(MYSQLN_TESTS) -unittest +@system unittest { - mixin(scopedCn); - cn.exec("DROP TABLE IF EXISTS `issue40`"); - cn.exec( - "CREATE TABLE `issue40` ( - `str` varchar(255) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8" - ); - - auto longString = repeat('a').take(251).array().idup; - cn.exec("INSERT INTO `issue40` VALUES('"~longString~"')"); - cn.query("SELECT * FROM `issue40`"); - - cn.exec("DELETE FROM `issue40`"); - - longString = repeat('a').take(255).array().idup; - cn.exec("INSERT INTO `issue40` VALUES('"~longString~"')"); - cn.query("SELECT * FROM `issue40`"); + static void test(bool isSafe)() + { + mixin(scopedCn); + mixin(doImports(isSafe, "commands")); + cn.exec("DROP TABLE IF EXISTS `issue40`"); + cn.exec( + "CREATE TABLE `issue40` ( + `str` varchar(255) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8" + ); + + auto longString = repeat('a').take(251).array().idup; + cn.exec("INSERT INTO `issue40` VALUES('"~longString~"')"); + cn.query("SELECT * FROM `issue40`"); + + cn.exec("DELETE FROM `issue40`"); + + longString = repeat('a').take(255).array().idup; + cn.exec("INSERT INTO `issue40` VALUES('"~longString~"')"); + cn.query("SELECT * FROM `issue40`"); + } + + test!false(); + () @safe { test!true(); }(); } // Issue #52: execSQLSequence doesn't work with map @("issue52") debug(MYSQLN_TESTS) -unittest +@system unittest { - mixin(scopedCn); + static void test(bool isSafe)() + { + mixin(scopedCn); + mixin(doImports(isSafe, "commands")); - assert(cn.query("SELECT 1").array.length == 1); - assert(cn.query("SELECT 1").map!(r => r).array.length == 1); - assert(cn.query("SELECT 1").array.map!(r => r).array.length == 1); + assert(cn.query("SELECT 1").array.length == 1); + assert(cn.query("SELECT 1").map!(r => r).array.length == 1); + assert(cn.query("SELECT 1").array.map!(r => r).array.length == 1); + } + + test!false(); + () @safe { test!true(); }(); } // Issue #56: Result set quantity does not equal MySQL rows quantity @("issue56") debug(MYSQLN_TESTS) -unittest +@system unittest { - mixin(scopedCn); - cn.exec("DROP TABLE IF EXISTS `issue56`"); - cn.exec("CREATE TABLE `issue56` (a datetime DEFAULT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - - cn.exec("INSERT INTO `issue56` VALUES - ('2015-03-28 00:00:00') - ,('2015-03-29 00:00:00') - ,('2015-03-31 00:00:00') - ,('2015-03-31 00:00:00') - ,('2015-03-31 00:00:00') - ,('2015-03-31 00:00:00') - ,('2015-04-01 00:00:00') - ,('2015-04-02 00:00:00') - ,('2015-04-03 00:00:00') - ,('2015-04-04 00:00:00')"); - - auto stmt = cn.prepare("SELECT a FROM `issue56`"); - auto res = cn.query(stmt).array; - assert(res.length == 10); + static void test(bool isSafe)() + { + mixin(scopedCn); + mixin(doImports(isSafe, "commands", "connection")); + cn.exec("DROP TABLE IF EXISTS `issue56`"); + cn.exec("CREATE TABLE `issue56` (a datetime DEFAULT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + + cn.exec("INSERT INTO `issue56` VALUES + ('2015-03-28 00:00:00') + ,('2015-03-29 00:00:00') + ,('2015-03-31 00:00:00') + ,('2015-03-31 00:00:00') + ,('2015-03-31 00:00:00') + ,('2015-03-31 00:00:00') + ,('2015-04-01 00:00:00') + ,('2015-04-02 00:00:00') + ,('2015-04-03 00:00:00') + ,('2015-04-04 00:00:00')"); + + auto stmt = cn.prepare("SELECT a FROM `issue56`"); + auto res = cn.query(stmt).array; + assert(res.length == 10); + } + + test!false(); + () @safe { test!true(); }(); } // Issue #66: Can't connect when omitting default database @("issue66") debug(MYSQLN_TESTS) -unittest +@safe unittest { + import mysql.connection; auto a = Connection.parseConnectionString(testConnectionStr); { @@ -205,75 +254,100 @@ unittest // Issue #117: Server packet out of order when Prepared is destroyed too early @("issue117") debug(MYSQLN_TESTS) -unittest +@system unittest { - mixin(scopedCn); - - struct S + static void test(bool isSafe)() { - this(ResultRange x) { r = x; } // destroying x kills the range - ResultRange r; - alias r this; - } + mixin(scopedCn); + mixin(doImports(isSafe, "result", "commands")); - cn.exec("DROP TABLE IF EXISTS `issue117`"); - cn.exec("CREATE TABLE `issue117` (a INTEGER) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - cn.exec("INSERT INTO `issue117` (a) VALUES (1)"); + struct S + { + this(ResultRange x) { r = x; } // destroying x kills the range + ResultRange r; + alias r this; + } - auto r = cn.query("SELECT * FROM `issue117`"); - assert(!r.empty); + cn.exec("DROP TABLE IF EXISTS `issue117`"); + cn.exec("CREATE TABLE `issue117` (a INTEGER) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + cn.exec("INSERT INTO `issue117` (a) VALUES (1)"); - auto s = S(cn.query("SELECT * FROM `issue117`")); - assert(!s.empty); + auto r = cn.query("SELECT * FROM `issue117`"); + assert(!r.empty); + + auto s = S(cn.query("SELECT * FROM `issue117`")); + assert(!s.empty); + } + + test!false(); + () @safe { test!true(); }(); } // Issue #133: `queryValue`: result of 1 row & field `NULL` check inconsistency / error @("issue133") debug(MYSQLN_TESTS) -unittest +@system unittest { - mixin(scopedCn); - - cn.exec("DROP TABLE IF EXISTS `issue133`"); - cn.exec("CREATE TABLE `issue133` (a BIGINT UNSIGNED NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - cn.exec("INSERT INTO `issue133` (a) VALUES (NULL)"); - - auto prep = cn.prepare("SELECT a FROM `issue133`"); - auto value = cn.queryValue(prep); + static void test(bool isSafe)() + { + mixin(scopedCn); + mixin(doImports(isSafe, "commands", "connection")); + import mysql.types; + + cn.exec("DROP TABLE IF EXISTS `issue133`"); + cn.exec("CREATE TABLE `issue133` (a BIGINT UNSIGNED NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + cn.exec("INSERT INTO `issue133` (a) VALUES (NULL)"); + + auto prep = cn.prepare("SELECT a FROM `issue133`"); + auto value = cn.queryValue(prep); + + assert(!value.isNull); + static if(isSafe) + assert(value.get.kind == MySQLVal.Kind.Null); + else + assert(value.get.type == typeid(typeof(null))); + } - assert(!value.isNull); - assert(value.get.type == typeid(typeof(null))); + test!false(); + () @safe { test!true(); }(); } // Issue #139: Server packet out of order when Prepared is destroyed too early @("issue139") debug(MYSQLN_TESTS) -unittest +@system unittest { - mixin(scopedCn); - - // Sanity check + static void test(bool isSafe)() { - ResultRange result; - - auto prep = cn.prepare("SELECT ?"); - prep.setArgs("Hello world"); - result = cn.query(prep); + mixin(scopedCn); + mixin(doImports(isSafe, "result", "commands", "connection")); - result.close(); - } - - // Should not throw server packet out of order - { - ResultRange result; + // Sanity check { + ResultRange result; + auto prep = cn.prepare("SELECT ?"); prep.setArgs("Hello world"); result = cn.query(prep); + + result.close(); } - result.close(); + // Should not throw server packet out of order + { + ResultRange result; + { + auto prep = cn.prepare("SELECT ?"); + prep.setArgs("Hello world"); + result = cn.query(prep); + } + + result.close(); + } } + + test!false(); + () @safe { test!true(); }(); } /+ @@ -292,37 +366,53 @@ So, this test ensures lockConnection doesn't return a connection that's already @("issue170") version(Have_vibe_core) debug(MYSQLN_TESTS) -unittest +@system unittest { - import mysql.safe.commands; - import mysql.safe.pool; - int count=0; + static void test(bool isSafe)() + { + mixin(doImports(isSafe, "connection", "commands", "pool")); - auto pool = new MySQLPool(testConnectionStr); - pool.onNewConnection = (Connection conn) { count++; }; - assert(count == 0); + int count=0; - auto cn1 = pool.lockConnection(); - assert(count == 1); + auto pool = new MySQLPool(testConnectionStr); + static if(isSafe) + pool.onNewConnection = (Connection conn) @safe { count++; }; + else + pool.onNewConnection = (Connection conn) @system { count++; }; + assert(count == 0); - auto cn2 = pool.lockConnection(); - assert(count == 2); + auto cn1 = pool.lockConnection(); + assert(count == 1); - assert(cn1 != cn2); + auto cn2 = pool.lockConnection(); + assert(count == 2); + + assert(cn1 !is cn2); + } + + test!false(); + () @safe { test!true(); }(); } // @("timestamp") debug(MYSQLN_TESTS) -unittest +@system unittest { - import mysql.types; - mixin(scopedCn); + static void test(bool isSafe)() + { + import mysql.types; + mixin(scopedCn); + mixin(doImports(isSafe, "commands", "connection")); + + cn.exec("DROP TABLE IF EXISTS `issueX`"); + cn.exec("CREATE TABLE `issueX` (a TIMESTAMP) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - cn.exec("DROP TABLE IF EXISTS `issueX`"); - cn.exec("CREATE TABLE `issueX` (a TIMESTAMP) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + auto stmt = cn.prepare("INSERT INTO `issueX` (`a`) VALUES (?)"); + stmt.setArgs(Timestamp(2011_11_11_12_20_02UL)); + cn.exec(stmt); + } - auto stmt = cn.prepare("INSERT INTO `issueX` (`a`) VALUES (?)"); - stmt.setArgs(Timestamp(2011_11_11_12_20_02UL)); - cn.exec(stmt); + test!false(); + () @safe { test!true(); }(); } diff --git a/source/mysql/types.d b/source/mysql/types.d index 6084e1c5..a67f3cf1 100644 --- a/source/mysql/types.d +++ b/source/mysql/types.d @@ -194,7 +194,7 @@ package MySQLVal _toVal(Variant v) import std.traits; import mysql.exceptions; alias BasicTypes = AliasSeq!(bool, byte, ubyte, short, ushort, int, uint, long, ulong, float, double, DateTime, TimeOfDay, Date, Timestamp); - alias ArrayTypes = AliasSeq!(char, ubyte); + alias ArrayTypes = AliasSeq!(char[], const(char)[], ubyte[], const(ubyte)[], immutable(ubyte)[]); switch (ts) { static foreach(Type; BasicTypes) @@ -210,15 +210,24 @@ package MySQLVal _toVal(Variant v) } static foreach(Type; ArrayTypes) { - case fullyQualifiedName!Type ~ "[]": - case "const(" ~ fullyQualifiedName!Type ~ ")[]": - case "immutable(" ~ fullyQualifiedName!Type ~ ")[]": - case "shared(immutable(" ~ fullyQualifiedName!Type ~ "))[]": - if(isRef) - return MySQLVal(v.get!(const(Type[]*))); - else - return MySQLVal(v.get!(const(Type[]))); + case Type.stringof: + { + alias ET = Unqual!(typeof(Type.init[0])); + if(isRef) + return MySQLVal(v.get!(const(ET[]*))); + else + return MySQLVal(v.get!(Type)); + } } + case "immutable(char)[]": + // have to do this separately, because everything says "string" but + // Variant says "immutable(char)[]" + if(isRef) + return MySQLVal(v.get!(const(char[]*))); + else + return MySQLVal(v.get!(string)); + case "typeof(null)": + return MySQLVal(null); default: throw new MYX("Unsupported Database Variant Type: " ~ ts); } @@ -258,6 +267,9 @@ TaggedAlgebraic version did not. All shims other than `type` will likely remain as convenience features. +Note that `peek` is inferred @system because it returns a pointer to the +provided value. + $(SAFE_MIGRATION) +/ bool convertsTo(T)(ref MySQLVal val) diff --git a/source/mysql/unsafe/commands.d b/source/mysql/unsafe/commands.d index 5c5db712..8e2aef27 100644 --- a/source/mysql/unsafe/commands.d +++ b/source/mysql/unsafe/commands.d @@ -134,9 +134,6 @@ unittest /++ Execute an SQL command or prepared statement, such as INSERT/UPDATE/CREATE/etc. -Note: The safe `mysql.safe.commands.exec` is also aliased here, so you have access to all -those overloads in addition to these. - This method is intended for commands such as which do not produce a result set (otherwise, use one of the `query` functions instead.) If the SQL command does produces a result set (such as SELECT), `mysql.exceptions.MYXResultRecieved` @@ -200,10 +197,29 @@ ulong exec(Connection conn, ref BackwardCompatPrepared prepared) @system return result; } -//ditto -// Note: doesn't look right in ddox, so I removed this as a ditto. -alias exec = SC.exec; +///ditto +ulong exec(Connection conn, ref Prepared prepared) @system +{ + return SC.exec(conn, prepared.safeForExec); +} + +///ditto +ulong exec(T...)(Connection conn, ref Prepared prepared, T args) + if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[])) +{ + // we are about to set all args, which will clear any parameter specializations. + prepared.setArgs(args); + return SC.exec(conn, prepared.safe); +} +// Note: this is a wrapper for the safe commands exec functions that do not +// involve a Prepared struct directly. +///ditto +@safe ulong exec(T...)(Connection conn, const(char[]) sql, T args) + if(!is(T[0] == Variant[])) +{ + return SC.exec(conn, sql, args); +} /++ Execute an SQL SELECT command or prepared statement. @@ -286,13 +302,15 @@ UnsafeResultRange query(Connection conn, const(char[]) sql, Variant[] args) @sys ///ditto UnsafeResultRange query(Connection conn, ref Prepared prepared) @system { - return SC.query(conn, prepared).unsafe; + return SC.query(conn, prepared.safeForExec).unsafe; } ///ditto -UnsafeResultRange query(T...)(Connection conn, ref Prepared prepared, T args) @system +UnsafeResultRange query(T...)(Connection conn, ref Prepared prepared, T args) if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) { - return SC.query(conn, prepared, args).unsafe; + // this is going to clear any parameter specialization + prepared.setArgs(args); + return SC.query(conn, prepared.safe, args).unsafe; } ///ditto UnsafeResultRange query(Connection conn, ref Prepared prepared, Variant[] args) @system @@ -394,13 +412,14 @@ Nullable!UnsafeRow queryRow(Connection conn, const(char[]) sql, Variant[] args) ///ditto Nullable!UnsafeRow queryRow(Connection conn, ref Prepared prepared) @system { - return SC.queryRow(conn, prepared).unsafe; + return SC.queryRow(conn, prepared.safeForExec).unsafe; } ///ditto Nullable!UnsafeRow queryRow(T...)(Connection conn, ref Prepared prepared, T args) @system if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) { - return SC.queryRow(conn, prepared, args).unsafe; + prepared.setArgs(args); + return SC.queryRow(conn, prepared.safe, args).unsafe; } ///ditto Nullable!UnsafeRow queryRow(Connection conn, ref Prepared prepared, Variant[] args) @system @@ -444,12 +463,22 @@ sql = The SQL command to be run. prepared = The prepared statement to be run. args = The variables, taken by reference, to receive the values. +/ -alias queryRowTuple = SC.queryRowTuple; +void queryRowTuple(T...)(Connection conn, const(char[]) sql, ref T args) +{ + return SC.queryRowTuple(conn, sql, args); +} + +///ditto +void queryRowTuple(T...)(Connection conn, ref Prepared prepared, ref T args) +{ + SC.queryRowTuple(conn, prepared.safeForExec, args); +} + ///ditto void queryRowTuple(T...)(Connection conn, ref BackwardCompatPrepared prepared, ref T args) @system { auto p = prepared.prepared; - .queryRowTuple(conn, p, args); + SC.queryRowTuple(conn, p.safeForExec, args); prepared._prepared = p; } @@ -545,13 +574,14 @@ Nullable!Variant queryValue(Connection conn, const(char[]) sql, Variant[] args) ///ditto Nullable!Variant queryValue(Connection conn, ref Prepared prepared) @system { - return SC.queryValue(conn, prepared).asVariant; + return SC.queryValue(conn, prepared.safeForExec).asVariant; } ///ditto Nullable!Variant queryValue(T...)(Connection conn, ref Prepared prepared, T args) @system if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) { - return SC.queryValue(conn, prepared, args).asVariant; + prepared.setArgs(args); + return queryValue(conn, prepared); } ///ditto Nullable!Variant queryValue(Connection conn, ref Prepared prepared, Variant[] args) @system