List:MySQL++« Previous MessageNext Message »
From:Waba Date:February 4 2006 8:18am
Subject:Patch: Nullable fields in SQLSS
View as plain text  
Here is my patch that enables one to use nullable fields in SQLSS. Since
it will break binary compatibility, I feel necessary to explain the
steps that I have followed. If you don't care, just skip to the summary
below.


WALKTHROUGH:

Let's try to compile a test program that contains a SQLSS with fields of
type mysqlpp::Null<something>. As expected we get compiler errors. It
complains that

 Null<T>::Null (const ColData)

is ambiguous, because it matches either the ctor that takes a T, or the
ctor that takes another Null<T>. Indeed, both calls are resolved through
the implicit conversion operators of ColData (to T - int, let's say, and
to Null<T>). IMHO, making these ops was a mistake (even back in v0.6x),
and it's too late to change it. See section 11.4 of Stroustrup's TC++PL
(3rd ed.) for a full discussion.

We have to be more explicit here, and there are several paths awaiting
our exploration: we could make Null constructible from ColData directly,
but that would lead to an include loop as each class would need a
full definition of the other. We cannot kill the implicit conversion
operators of ColData to T as they are required for backwards source
compatibility. OTOH, the conversion to Null has got to stay in some
form inside the ColData class, as it is alone to know whether its
content is null or not. (2) We can however replace this conversion
by a real function, as it is useless in its current form (except for
Null<string>, as ColData inherits from Str, another early mistake, see
section 12.4.2).

Having removed this operator, implicitly constructing a Null<string>
from a ColData is now impossible. However, it works for Null<int>, which
is very bad as the is-null information is lost on the way! We have to
resort to some trick to forbid both operations: we define the operator
(blocking all but string), and write an instruction inside that will
fail for string:

 operator Null<T, B> () { T (1)/1; return get_null<T, B> (); }

This is the kind of tricks used in Boost.MPL. Now, constructing a Null
from a ColData is homogeneously impossible for all types, which is a
Good Thing. The user can still however write:

 Null<T, B> n (col_data.get_null<T, B> ());

(3) At this point the compiler will actually attempt to process the code
inside get_null(), and will scream about another ambiguous conversion
in the manual comparison to the string "NULL". This may not be the case
everywhere however, as g++-4.0 states that

 ISO C++ says that these are ambiguous, even though the worst conversion
 for the first is better than the worst conversion for the second.

Anyway, the problem is easily fixed: instead of calling these implicit
operators via "(*this)[0]", static_cast to the base Str type first.

(8) Ok, now we can compile calls to get_null<T, B> () for T numeric,
but it's still broken for strings and date types, as there is no
mysql_convert template code for them. Easily fixed too, not forgetting
to update the "end" parameter accordingly. However, that leads to
DateTime needing the full definition of ColData (since it's only a
typedef) and ColData needing DateTime to produce Null<DateTime>, so
we've got to break the circle one way or another. No choice here: we
have to remove the Date/Time-from-ColData ctors. We could attempt
to retain source compatibility by defining more implicit conversion
operators inside ColData, however that would break in some cases.

(1) Rather, we template the string ctors of DateTime, therefore
making them usable for ColData aswell, avoiding to include coldata.h
and solving all our problems. Now everything compiles again, except
SSQLSes that have Nulls inside ;-) (6) Human coders can manually call
get_null(), however the macros defined by custom.pl can't do that
without resorting to even more template tricks.

We can however modify custom.pl to call the general explicit conversion
method, conv(), instead of forcing an implicit conversion through
static_cast. Now the compiler reminds us that we forgot to specialize
(overload, actually) conv() to handle Nulls. This is easily done
through:

 template <class T, class B> Null<T, B> conv (Null<T, B>) const

which in turn calls get_null(). We are 100% sure that the right function
gets called every time, as the wrong one wouldn't compile.

(4) Not totally related, but when making a Null<string>, the only
correct (as in "compiles") Behavior parameter is NullisBlank, which is
not the default. OTOH, the comma in Null<string, NullisBlank> breaks the
macro call, so the users will have to use a typedef anyway. Rather than
forcing everyone to do so, let's define it ourselves in null.h.

(7) Anticipating on what we're supposed to discover only by running our
sample code, we need to refine the quoting system to accommodate for
Null templates.

Et voilà! Now the last thing left to do is an improved custom7.cpp that
proves the correct behaviour of our patch. Thank you for reading so far.


SUMMARY: (The numbers below refer to those above)

Here is a summary of the changes. For a discussion on the reasons behind
them, please read the above.
1) DateTime loses its ColData constructor, which is replaced by a
   template ctor that works for all strings and ColData variants.
2) The non-functional conversion operator to Null in ColData is made
   unusable for all types. It is replaced by an explicit get_null()
   function and an overload of conv().
3) Modification in the get_null() function so that it compiles on the
   strict g++-4.0
4) Added a NullString typedef for user convenience, as it must exist to
   use null strings in SSQLSs.
5) Made the desired ostream format explicit in datetime.cpp. Unsure why
   change 1 (I think) made this necessary.
6) In the populate code of the SSQLSs, use explicit conversion through
   conv() rather than relying on implicit conversion operators.
7) Handle Null templates correctly in the quoting manipulators.
8) Added mysql_convert template instantiations for strings and time
   types.

This patch has been tested under g++ 3.3, 3.4 and 4.0. Please test it
under VC++ as I have no access this platform. Please also test that 
real-world applications still compile with it (I don't see how run-time
behaviour could be affected).


Best regards,
-Waba.

Index: lib/datetime.h
===================================================================
--- lib/datetime.h	(revision 1163)
+++ lib/datetime.h	(working copy)
@@ -31,7 +31,6 @@
 
 #include "defs.h"
 
-#include "coldata.h"
 #include "stream2string.h"
 #include "tiny_int.h"
 
@@ -164,19 +163,12 @@
 	/// \brief Initialize object from a MySQL date-and-time string
 	///
 	/// \sa DateTime(cchar*)
-	DateTime(const ColData& str)
+	template<class Str>
+	DateTime(const Str &str)
 	{
 		convert(str.c_str());
 	}
 
-	/// \brief Initialize object from a MySQL date-and-time string
-	///
-	/// \sa DateTime(cchar*)
-	DateTime(const std::string& str)
-	{
-		convert(str.c_str());
-	}
-
 	/// \brief Compare this datetime to another.
 	///
 	/// Returns < 0 if this datetime is before the other, 0 of they are
@@ -259,16 +251,9 @@
 	/// \brief Initialize object from a MySQL date string
 	///
 	/// \sa Date(cchar*)
-	Date(const ColData& str) { convert(str.c_str()); }
-
-	/// \brief Initialize object from a MySQL date string
-	///
-	/// \sa Date(cchar*)
-	Date(const std::string& str)
-	{
-		convert(str.c_str());
-	}
-
+	template<class Str>
+	Date(const Str& str) { convert(str.c_str()); }
+	
 	/// \brief Compare this date to another.
 	///
 	/// Returns < 0 if this date is before the other, 0 of they are
@@ -342,16 +327,9 @@
 	/// \brief Initialize object from a MySQL time string
 	///
 	/// \sa Time(cchar*)
-	Time(const ColData& str) { convert(str.c_str()); }
+	template<class Str>
+	Time(const Str& str) { convert(str.c_str()); }
 
-	/// \brief Initialize object from a MySQL time string
-	///
-	/// \sa Time(cchar*)
-	Time(const std::string& str)
-	{
-		convert(str.c_str());
-	}
-
 	/// \brief Parse a MySQL time string into this object.
 	MYSQLPP_EXPORT cchar* convert(cchar*);
 
Index: lib/coldata.h
===================================================================
--- lib/coldata.h	(revision 1163)
+++ lib/coldata.h	(working copy)
@@ -149,6 +149,9 @@
 	/// \brief Template for converting data from one type to another.
 	template <class Type> Type conv(Type dummy) const;
 
+	template <class T, class B> Null<T, B> conv (Null<T, B>) const
+			{ return get_null<T, B> (); }
+	
 	/// \brief Set a flag indicating that this object is a SQL null.
 	void it_is_null() { null_ = true; }
 
@@ -161,6 +164,19 @@
 		return buf_;
 	}
 	
+	/** \brief Convert to a nullable type.
+	 *
+	 * You have to explicitely provide the template types to the function,
+	 * like this:
+	 * <code>
+	 * Null<T, B> n (col_data.get_null<T, B> ());
+	 * </code>
+	 *
+	 * Or use conv(), to which you can give a sample of the desired
+	 * return type.
+	 */
+	template <class T, class B> Null<T, B> get_null () const;
+
 	/// \brief Returns a const char pointer to the string form of
 	/// this object's data.
 	operator cchar*() const { return buf_.c_str(); }
@@ -222,8 +238,19 @@
 	/// \brief Converts this object's string data to a bool
 	operator bool() const { return conv(0); }
 
-	template <class T, class B> operator Null<T, B>() const;
-
+	/** \brief Dummy conversion to Null.
+	 *
+	 * This is meant to be ambiguous to the compiler. Without this trick,
+	 * Null would be constructible from the other implicit conversion 
+	 * operators, and would in the end be counter-intuitive to the user
+	 * who would lose the is-null information. This way, constructing a 
+	 * Null from a ColData implicitely is impossible.
+	 *
+	 * Use get_null() or conv() explicitely instead.
+	 */
+	template<class T, class B> operator Null<T, B> () const 
+			{ T (1)/1; return get_null<T, B> (); }
+	
 private:
 	mysql_type_info type_;
 	std::string buf_;
@@ -295,13 +322,13 @@
 /// the object is exactly equal to "NULL".  Else, it constructs an empty
 /// object of type T and tries to convert it to Null<T, B>.
 template <class Str> template<class T, class B>
-ColData_Tmpl<Str>::operator Null<T, B>() const
+Null<T, B> ColData_Tmpl<Str>::get_null () const
 {
 	if ((Str::size() == 4) &&
-			(*this)[0] == 'N' &&
-			(*this)[1] == 'U' &&
-			(*this)[2] == 'L' &&
-			(*this)[3] == 'L') {
+		static_cast<Str>(*this)[0] == 'N' &&
+		static_cast<Str>(*this)[1] == 'U' &&
+		static_cast<Str>(*this)[2] == 'L' &&
+		static_cast<Str>(*this)[3] == 'L') {
 		return Null<T, B>(null);
 	}
 	else {
Index: lib/null.h
===================================================================
--- lib/null.h	(revision 1163)
+++ lib/null.h	(working copy)
@@ -35,6 +35,7 @@
 #include "exceptions.h"
 
 #include <iostream>
+#include <string>
 
 namespace mysqlpp {
 
@@ -273,6 +274,9 @@
 		return o << n.data;
 }
 
+/// \brief The correct Null template types for nullable STL strings
+typedef Null<std::string, NullisBlank> NullString;
+
 } // end namespace mysqlpp
 
 #endif
Index: lib/datetime.cpp
===================================================================
--- lib/datetime.cpp	(revision 1163)
+++ lib/datetime.cpp	(working copy)
@@ -40,9 +40,9 @@
 {
 	char fill = os.fill('0');
 	ios::fmtflags flags = os.setf(ios::right);
-	os		<< setw(4) << d.year << '-' 
-			<< setw(2) << d.month << '-'
-			<< setw(2) << d.day;
+	os		<< setw(4) << static_cast<int>(d.year) << '-'
+			<< setw(2) << static_cast<int>(d.month) << '-'
+			<< setw(2) << static_cast<int>(d.day);
 	os.flags(flags);
 	os.fill(fill);
 	return os;
@@ -53,9 +53,9 @@
 {
 	char fill = os.fill('0');
 	ios::fmtflags flags = os.setf(ios::right);
-	os		<< setw(2) << t.hour << ':' 
-			<< setw(2) << t.minute << ':'
-			<< setw(2) << t.second;
+	os		<< setw(2) << static_cast<int>(t.hour) << ':' 
+			<< setw(2) << static_cast<int>(t.minute) << ':'
+			<< setw(2) << static_cast<int>(t.second);
 	os.flags(flags);
 	os.fill(fill);
 	return os;
Index: lib/custom.pl
===================================================================
--- lib/custom.pl	(revision 1163)
+++ lib/custom.pl	(working copy)
@@ -262,7 +262,7 @@
 	$parm_simple2c_b .= ", " unless $j == $i;
 	$defs  .= "    T$j I$j;";
 	$defs  .= "\n" unless $j == $i;
-	$popul .= "    s->I$j = static_cast<T$j>(row.at(O$j));";
+	$popul .= "    s->I$j = row.at(O$j).conv (T$j ());";
 	$popul .= "\n" unless $j == $i;
         $names .= "    N$j ";
 	$names .= ",\n" unless $j == $i;
Index: lib/manip.h
===================================================================
--- lib/manip.h	(revision 1163)
+++ lib/manip.h	(working copy)
@@ -201,6 +201,18 @@
 	return *o.ostr << '\'' << in << '\'';
 }
 
+
+template <class T, class B>
+MYSQLPP_EXPORT inline std::ostream& operator <<(quote_type1 o,
+		const Null<T, B>& in)
+{
+	if (in.is_null)
+		return *o.ostr << "NULL";
+	else
+		return o << in.data; // quote again
+}
+
+
 #endif // !defined(DOXYGEN_IGNORE)
 
 
@@ -313,6 +325,18 @@
 	return *o.ostr << '\'' << in << '\'';
 }
 
+
+template <class T, class B>
+MYSQLPP_EXPORT inline std::ostream& operator <<(quote_only_type1 o,
+		const Null<T, B>& in)
+{
+	if (in.is_null)
+		return *o.ostr << "NULL";
+	else
+		return o << in.data; // quote again
+}
+
+
 #endif // !defined(DOXYGEN_IGNORE)
 
 
@@ -427,6 +451,18 @@
 	return *o.ostr << '"' << in << '"';
 }
 
+
+template <class T, class B>
+MYSQLPP_EXPORT inline std::ostream& operator <<(quote_double_only_type1 o,
+		const Null<T, B>& in)
+{
+	if (in.is_null)
+		return *o.ostr << "NULL";
+	else
+		return o << in.data; // quote again
+}
+
+
 #endif // !defined(DOXYGEN_IGNORE)
 
 
Index: lib/convert.h
===================================================================
--- lib/convert.h	(revision 1163)
+++ lib/convert.h	(working copy)
@@ -34,8 +34,10 @@
 #include "platform.h"
 
 #include "defs.h"
+#include "datetime.h"
 
 #include <stdlib.h>
+#include <string>
 
 namespace mysqlpp {
 
@@ -113,6 +115,63 @@
 
 #endif // !defined(DOXYGEN_IGNORE)
 
+template <>
+class mysql_convert<std::string>
+{
+	std::string m_str;
+	
+public:
+	mysql_convert (const char* str, const char *& end) :
+	m_str (str)
+	{
+		end = str + m_str.size ();
+	}
+	
+	operator std::string () { return m_str; }
+};
+
+template <>
+class mysql_convert<Date>
+{
+	Date m_data;
+	
+public:
+	mysql_convert (const char* str, const char *& end)
+	{
+		end = m_data.convert (str);
+	}
+	
+	operator Date () { return m_data; }
+};
+
+template <>
+class mysql_convert<DateTime>
+{
+	DateTime m_data;
+	
+public:
+	mysql_convert (const char* str, const char *& end)
+	{
+		end = m_data.convert (str);
+	}
+	
+	operator DateTime () { return m_data; }
+};
+
+template <>
+class mysql_convert<Time>
+{
+	Time m_data;
+	
+public:
+	mysql_convert (const char* str, const char *& end)
+	{
+		end = m_data.convert (str);
+	}
+	
+	operator Time () { return m_data; }
+};
+
 } // end namespace mysqlpp
 
 #endif
Index: examples/Makefile.am
===================================================================
--- examples/Makefile.am	(revision 1163)
+++ examples/Makefile.am	(working copy)
@@ -27,7 +27,7 @@
 
 noinst_PROGRAMS = resetdb simple1 simple2 simple3 usequery multiquery \
 		custom1 custom2 custom3 custom4 custom5 custom6 fieldinf1 \
-		dbinfo updel load_file cgi_image
+		dbinfo updel load_file cgi_image custom7
 
 noinst_HEADERS = util.h
 
@@ -70,6 +70,9 @@
 custom6_SOURCES = custom6.cpp util.cpp
 custom6_DEPENDENCIES = $(MYSQLPP_LIB)
 
+custom7_SOURCES = custom7.cpp util.cpp
+custom7_DEPENDENCIES = $(MYSQLPP_LIB)
+
 fieldinf1_SOURCES = fieldinf1.cpp util.cpp
 fieldinf1_DEPENDENCIES = $(MYSQLPP_LIB)
 
Index: examples/resetdb.cpp
===================================================================
--- examples/resetdb.cpp	(revision 1163)
+++ examples/resetdb.cpp	(working copy)
@@ -60,6 +60,7 @@
 			// really care, as it'll get created next.
 			cout << "Dropping existing stock table..." << endl;
 			query.execute("drop table stock");
+			query.execute("drop table stock2");
 		}
 		else {
 			// Database doesn't exist yet, so create and select it.
@@ -105,6 +106,12 @@
 
 		cout << (new_db ? "Created" : "Reinitialized") <<
 				" sample database successfully." << endl;
+	
+		// The stock2 table introduces a null string, needed to test
+		// custom7.
+		cout << "Creating new stock2 table..." << endl;
+		query.execute ("create table stock2 (id int primary key, "
+		               " name varchar(20), qtty int, eta date)");
 	}
 	catch (const mysqlpp::BadQuery& er) {
 		// Handle any query errors

Attachment: [application/pgp-signature] Digital signature signature.asc
Thread
Patch: Nullable fields in SQLSSWaba4 Feb
  • Re: Patch: Nullable fields in SQLSSChris Frey4 Feb
    • Re: Patch: Nullable fields in SQLSSWaba4 Feb
  • Re: Patch: Nullable fields in SQLSSWarren Young6 Feb
  • Re: Patch: Nullable fields in SQLSSWarren Young5 Nov
Re: Patch: Nullable fields in SQLSSddneilson7 Nov
  • Re: Patch: Nullable fields in SQLSSWarren Young7 Nov
Re: Patch: Nullable fields in SQLSSDavid A. Betz14 Nov
  • Re: Patch: Nullable fields in SQLSSWarren Young14 Nov
Re: Patch: Nullable fields in SQLSSDavid A. Betz17 Nov
  • Re: Patch: Nullable fields in SQLSSWarren Young18 Nov