Last active
May 8, 2016 04:21
-
-
Save Kludgy/99b6b30b0fb12cc19c9f559a1ef784d6 to your computer and use it in GitHub Desktop.
One approach to the typing row data in C++11.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
One possible approach to typing SQL result sets in C++11. | |
TODO: Nominal row type APIs to disambiguate different row schemas that happen to have the same field descriptions. | |
*/ | |
#include <iostream> | |
#include <string> | |
#include <sstream> | |
#include <vector> | |
using std::cout; | |
using std::endl; | |
using std::make_pair; | |
using std::ostream; | |
using std::pair; | |
using std::string; | |
using std::stringstream; | |
using std::tuple; | |
using std::vector; | |
/* | |
Some mock data to play with, mimicking naive SQL result set APIs. Same principles apply to any, | |
- Input data that is poorly typed (here, vector<vector<string>>). | |
- A data schema (here, vector<sql_field_desc>) that provides the context for interpreting data. | |
*/ | |
enum sql_field_type | |
{ | |
sql_field_type_datetime, | |
sql_field_type_int, | |
sql_field_type_varchar | |
}; | |
struct sql_field_type_desc | |
{ | |
sql_field_type type; | |
bool is_signed; | |
}; | |
struct sql_field_desc | |
{ | |
string name; | |
sql_field_type_desc type_desc; | |
}; | |
struct sql_queryresult | |
{ | |
vector<sql_field_desc> fields; | |
vector< vector<string> > data; // data[row][col] yields field data | |
}; | |
static sql_queryresult sql_mock_queryresult_Customer() | |
{ | |
return | |
{ | |
{ | |
{ "CustomerID", sql_field_type_int, true }, | |
{ "CustomerName", sql_field_type_varchar, false }, | |
{ "BirthDate", sql_field_type_datetime, false } | |
}, | |
{ | |
{ "1001", "Joe Bluff", "1543-01-07 00:00:00" }, | |
{ "1002", "Jenny Guff", "3232-32-32 00:00:00" }, | |
{ "1003", "Guy Fluff", "1988-12-31 00:00:00" }, | |
{ "1004", "Matilda Huff", "1989-01-01 00:00:00" } | |
} | |
}; | |
} | |
static sql_queryresult sql_mock_queryresult_Purchase() | |
{ | |
return | |
{ | |
{ | |
{ "PurchaseID", sql_field_type_int, true }, | |
{ "PurchaseDate", sql_field_type_datetime, false } | |
}, | |
{ | |
{ "9991", "2016-01-13 23:33:12" }, | |
{ "9992", "2016-01-13 23:38:03" } | |
} | |
}; | |
} | |
/* unsaturated row */ | |
template <class...> struct DBRowType {}; | |
/* field labels */ | |
class BirthDate{}; | |
class CustomerID{}; | |
class CustomerName{}; | |
class PurchaseID{}; | |
class PurchaseDate{}; | |
/* saturated rows */ | |
typedef DBRowType< CustomerID, CustomerName, BirthDate > CustomerRow; | |
typedef DBRowType< PurchaseID, PurchaseDate > PurchaseRow; | |
/* field type labels */ | |
struct DBDate{}; | |
struct DBInt{}; | |
struct DBString{}; | |
/* field -> type relation */ | |
template <class> class DBFieldType{}; | |
template <> struct DBFieldType<BirthDate > { typedef DBDate type; }; | |
template <> struct DBFieldType<CustomerID > { typedef DBInt type; }; | |
template <> struct DBFieldType<CustomerName> { typedef DBString type; }; | |
template <> struct DBFieldType<PurchaseID > { typedef DBInt type; }; | |
template <> struct DBFieldType<PurchaseDate> { typedef DBDate type; }; | |
/* field type -> native representation */ | |
template <class> struct DBNativeType{}; | |
template <> struct DBNativeType<DBDate > { typedef string type; }; | |
template <> struct DBNativeType<DBInt > { typedef int type; }; | |
template <> struct DBNativeType<DBString> { typedef string type; }; | |
/* injecting sql_field_type_desc -> field type */ | |
template <class> bool DBImpliesType(sql_field_type_desc); | |
template <> bool DBImpliesType<DBDate >(sql_field_type_desc d) { return d.type == sql_field_type_datetime; } | |
template <> bool DBImpliesType<DBInt >(sql_field_type_desc d) { return d.type == sql_field_type_int && d.is_signed; } | |
template <> bool DBImpliesType<DBString>(sql_field_type_desc d) { return d.type == sql_field_type_varchar; } | |
/* field names */ | |
template <class> struct DBFieldName{}; | |
template <> struct DBFieldName<BirthDate > { static constexpr char const * value = "BirthDate"; }; | |
template <> struct DBFieldName<CustomerID > { static constexpr char const * value = "CustomerID"; }; | |
template <> struct DBFieldName<CustomerName> { static constexpr char const * value = "CustomerName"; }; | |
template <> struct DBFieldName<PurchaseID > { static constexpr char const * value = "PurchaseID"; }; | |
template <> struct DBFieldName<PurchaseDate> { static constexpr char const * value = "PurchaseDate"; }; | |
/* inductive row value */ | |
template <class> struct DBRowValue{}; | |
template <class field, class... fields> struct DBRowValue< DBRowType<field, fields...> > | |
{ | |
typename DBNativeType< typename DBFieldType<field>::type >::type head; | |
DBRowValue< DBRowType<fields...> > tail; | |
}; | |
template <> struct DBRowValue< DBRowType<> > {}; | |
/* field -> value relation */ | |
template <class, class> struct DBGetFieldValue; | |
template <class target, class field, class... fields> struct DBGetFieldValue< target, DBRowType<field, fields...> > | |
{ | |
static constexpr auto get(DBRowValue< DBRowType<field, fields...> > row) -> decltype(DBGetFieldValue< target, DBRowType<fields...> >::get(row.tail)) | |
{ | |
return DBGetFieldValue< target, DBRowType<fields...> >::get(row.tail); | |
} | |
}; | |
template <class target, class... fields> struct DBGetFieldValue< target, DBRowType<target, fields...> > | |
{ | |
static constexpr typename DBNativeType< typename DBFieldType<target>::type >::type get(DBRowValue< DBRowType<target, fields...> > row) | |
{ | |
return row.head; | |
} | |
}; | |
template <class target> struct DBGetFieldValue< target, DBRowType<> > | |
{ | |
}; | |
/* nicer api */ | |
template <class rowtype> struct DBRow | |
{ | |
DBRowValue<rowtype> value; | |
DBRow(DBRowValue<rowtype> value) : value(value) {} | |
template <class field> auto get() const -> decltype(DBGetFieldValue<field, rowtype>::get(value)) | |
{ | |
return DBGetFieldValue<field, rowtype>::get(value); | |
} | |
}; | |
/* row constructor with type deduction */ | |
template <class... fields> DBRow< DBRowType<fields...> > DBMakeRow(DBRowValue< DBRowType<fields...> > value) | |
{ | |
return DBRow< DBRowType<fields...> >(value); | |
} | |
/* (toy example) field value parsing */ | |
template <class fieldtype> pair<bool, typename DBNativeType<fieldtype>::type> DBParseSQLFieldData(string value); | |
template <> pair<bool, int> DBParseSQLFieldData<DBInt>(string s) | |
{ | |
return make_pair(true, atoi(s.c_str())); | |
} | |
template <> pair<bool, string> DBParseSQLFieldData<DBDate>(string s) | |
{ | |
return make_pair(true, s); | |
} | |
template <> pair<bool, string> DBParseSQLFieldData<DBString>(string s) | |
{ | |
return make_pair(true, s); | |
} | |
/* inductive row type (schema) validation */ | |
template <class> struct DBValidateRowType; | |
template <class field, class... fields> struct DBValidateRowType< DBRowType<field, fields...> > | |
{ | |
static bool validate(vector<sql_field_desc> field_descs, size_t curfield) | |
{ | |
if (curfield >= field_descs.size()) | |
{ | |
return false; | |
// out of range-- row type larger than given row description | |
} | |
auto field_desc = field_descs[curfield]; | |
if (field_desc.name != DBFieldName<field>::value) | |
{ | |
return false; | |
// column name mismatch-- expected DBFieldName<field>::value, but got field_desc.name | |
} | |
if (!DBImpliesType<typename DBFieldType<field>::type>(field_desc.type_desc)) | |
{ | |
return false; | |
// column type mismatch-- field_desc does not imply mydesc | |
} | |
return DBValidateRowType< DBRowType<fields...> >::validate(field_descs, curfield+1); | |
} | |
}; | |
template <> struct DBValidateRowType< DBRowType<> > | |
{ | |
static bool validate(vector<sql_field_desc> field_descs, size_t curfield) | |
{ | |
if (curfield != field_descs.size()) | |
{ | |
return false; | |
// out of range-- row type smaller than given row description | |
} | |
return true; | |
} | |
}; | |
/* inductive row value parsing */ | |
template <class> struct DBParseRow; | |
template <class field, class... fields> struct DBParseRow< DBRowType<field, fields...> > | |
{ | |
static pair< bool, DBRowValue< DBRowType<field, fields...> > > parse(vector<string> data, size_t curcol) | |
{ | |
if (curcol >= data.size()) | |
{ | |
return make_pair(false, DBRowValue< DBRowType<field, fields...> >()); | |
// out of range-- row type larger than given row data-- result is garbage | |
} | |
auto head = DBParseSQLFieldData< typename DBFieldType<field>::type >(data[curcol]); | |
if (!head.first) | |
{ | |
return make_pair(false, DBRowValue< DBRowType<field, fields...> >()); | |
// parsing error in this value-- result is garbage | |
} | |
auto tail = DBParseRow< DBRowType<fields...> >::parse(data, curcol+1); | |
if (!tail.first) | |
{ | |
return make_pair(false, DBRowValue< DBRowType<field, fields...> >()); | |
// some parsing error occurred in our tail-- result is garbage | |
} | |
return make_pair(true, DBRowValue< DBRowType<field, fields...> >{ head.second, tail.second }); | |
} | |
}; | |
template <> struct DBParseRow< DBRowType<> > | |
{ | |
static pair< bool, DBRowValue< DBRowType<> > > parse(vector<string> data, size_t curcol) | |
{ | |
if (curcol != data.size()) | |
{ | |
return make_pair(false, DBRowValue< DBRowType<> >()); | |
// out of range-- row type smaller than given row data-- result is garbage | |
} | |
return make_pair(true, DBRowValue< DBRowType<> >{}); | |
} | |
}; | |
/* example api for row validate+parse */ | |
template <class ROWTYPE> pair< bool, vector< DBRow<ROWTYPE> > > DBBindQueryResult(sql_queryresult raw) | |
{ | |
typedef DBRow<ROWTYPE> ROW; | |
auto valid = DBValidateRowType<ROWTYPE>::validate(raw.fields, 0); | |
if (!valid) | |
{ | |
return make_pair(false, vector<ROW>{}); | |
// validation failure-- result is garbage | |
} | |
auto result = vector<ROW>(); | |
for (auto line : raw.data) | |
{ | |
auto bound = DBParseRow<ROWTYPE>::parse(line, 0); | |
if (!bound.first) | |
{ | |
return make_pair(false, vector<ROW>{}); | |
// row parse failure-- result is garbage | |
} | |
else | |
{ | |
result.push_back(DBMakeRow(bound.second)); | |
} | |
} | |
return make_pair(true, result); | |
} | |
/* demo ostream */ | |
template <class> struct DBFieldTypeName{}; | |
template <> struct DBFieldTypeName<DBDate > { static constexpr char const * value = "DBDate"; }; | |
template <> struct DBFieldTypeName<DBInt > { static constexpr char const * value = "DBInt"; }; | |
template <> struct DBFieldTypeName<DBString> { static constexpr char const * value = "DBString"; }; | |
template <class rowtype> struct DBPrintRowValue; | |
template <class field, class... fields> struct DBPrintRowValue< DBRowType<field, fields...> > | |
{ | |
static ostream& print(ostream& os, DBRowValue< DBRowType<field, fields...> > rowvalue) | |
{ | |
char const * const fieldname = DBFieldName<field>::value; | |
char const * const fieldtname = DBFieldTypeName< typename DBFieldType<field>::type >::value; | |
os << " " << fieldname << ": " << rowvalue.head << " (" << fieldtname << ")" << endl; | |
return DBPrintRowValue< DBRowType<fields...> >::print(os, rowvalue.tail); | |
} | |
}; | |
template <> struct DBPrintRowValue< DBRowType<> > | |
{ | |
static ostream& print(ostream& os, DBRowValue< DBRowType<> > rowvalue) | |
{ | |
return os; | |
} | |
}; | |
template <class... fields> ostream& operator <<(ostream& os, DBRow< DBRowType<fields...> > row) | |
{ | |
return DBPrintRowValue< DBRowType<fields...> >::print(os, row.value); | |
} | |
/* demonstration of binding */ | |
template <class ROWTYPE, class MAPROW> | |
void bind_demo(sql_queryresult data, MAPROW maprow) | |
{ | |
cout << "=============" << endl; | |
auto result = DBBindQueryResult<ROWTYPE>(data); | |
if (!result.first) | |
{ | |
// TODO: Implement error details. :) | |
cout << "Parse error" << endl; | |
return; | |
} | |
cout << "Parse OK" << endl; | |
if (result.second.empty()) | |
{ | |
cout << "(no row data available)" << endl; | |
return; | |
} | |
// Print all row data (field name, value, field type) | |
for (auto row : result.second) | |
{ | |
cout << "row:" << endl << row << endl; | |
} | |
cout << "Transform first row:" << endl; | |
cout << maprow(result.second[0]); | |
cout << endl; | |
} | |
int main() | |
{ | |
// Compile error: native / SQL row mismatch | |
// auto bad_result_1 = DBBindQueryResult<PurchaseRow>(sql_mock_queryresult_Customer()); | |
// Compile error: native row is smaller than SQL row | |
// typedef DBRowType<CustomerID, CustomerName> CustomerNameRow; | |
// auto bad_result_2 = DBBindQueryResult<CustomerNameRow>(sql_mock_queryresult_Customer()); | |
// Compile succeeds (generally sloppy admission of equivalence) because row type is structurally | |
// equivalent. | |
{ | |
typedef DBRowType<CustomerID, CustomerName, BirthDate> CustomerRow2; | |
auto ok_result_2 = DBBindQueryResult<CustomerRow2>(sql_mock_queryresult_Customer()); | |
} | |
bind_demo<CustomerRow>(sql_mock_queryresult_Customer(), [](DBRow<CustomerRow> row) -> string { | |
stringstream out; | |
out << row.get<CustomerName>() << " has a birthday on " << row.get<BirthDate>() << "." << endl; | |
return out.str(); | |
}); | |
bind_demo<PurchaseRow>(sql_mock_queryresult_Purchase(), [](DBRow<PurchaseRow> row) -> string { | |
stringstream out; | |
out << "Item " << row.get<PurchaseID>() << " purchased on " << row.get<PurchaseDate>() << "." << endl; | |
return out.str(); | |
}); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment