Defining Mapped Class Types

struct point {
    float x;
    float y;
QUINCE_MAP_CLASS(point, (x)(y))

struct movie {
    serial id;                  // primary key with automatically assigned values
    std::string title;
QUINCE_MAP_CLASS(movie, (id)(title))

struct cinema {
    serial id;                  // primary key with automatically assigned values
    point location;
QUINCE_MAP_CLASS(cinema, (id)(location))

struct screen {
    serial id;                  // primary key with automatically assigned values
    serial cinema_id;           // foreign key to cinemas table
    serial current_movie_id;    // foreign key to movies table
QUINCE_MAP_CLASS(screen, (id)(cinema_id)(current_movie_id))


Each invocation of the macro QUINCE_MAP_CLASS generates all the meta-information quince will need, regarding one of our class types. (We've declared them with the keyword struct, but structs are classes as far as quince is concerned.) This meta-information is packaged into a class, known as the mapper class for our class. So the first invocation of QUINCE_MAP_CLASS generates the definition of point's mapper class, aka class_mapper<point>.

It's called a mapper class because it tells quince how to map a point to its representation as columns (to accompany an SQL statement), and how to map columns back to a point (when retrieving results from an SQL statement). In particular, it tells quince that, in order to map a point p in any way, it must map the two floats p.x and p.y.

As we shall see, there are variations in the way points are represented in different contexts: the SQL column names can vary, the encoding of floats can vary. So different situations will call for different mapper objects for point. But they will all be instances of the mapper class that we have just defined.

We rarely mention mapper classes by name in application code, but there is something about them that we need to understand. class_mapper<point> has two public members also called x and y. If cmp is any class_mapper<point>, then, cmp.x and cmp.y refer to float mappers, which tell quince how to map a point's x and y members respectively. So it's nearly true to say the members of the mapper are the mappers of the members [3] .

Following the same pattern, if cmc is a class_mapper<cinema>, then and cmc.location refer to a serial mapper and a point mapper respectively. [4] .

Which brings us to serial. This is a C++ type, defined by quince, and it corresponds to whatever SQL type a database uses for primary keys with automatically assigned values. We use serial for items that are DBMS-generated (such as movie::id), and for items that will hold copies of those (such as screen::current_movie_id).

[3] Nearly true but not 100% true. In fact, the members of the mapper are const references to the mappers of the members. This is for the sake of polymorphism. At this stage, nobody knows exactly what type the mappers of the members should be: there are different mapper classes for float on different backends, and we haven't yet said which backend we're using. Also there is the possibility of custom mappings. At run time, when quince constructs cmp, it will have that information, and it will build the appropriate mapper objects for cmp.x and cmp.y to refer to. But at compile time, all that is known is that they refer to some mapper of float -- which is an abstract type (abstract_mapper<float>, in fact).

[4] Readers of the previous footnote will be sensitive to a subtle difference in this case. We do know, at compile-time, the exact type of cmc.location: it's a class_mapper<point>, and that type is already fully defined. So quince does not need to use polymorphism in this case, and in fact it chooses not to. This subtlety affects what you can write in application code. E.g. if your code gets hold of a reference to cmc (and we shall soon see how that can happen), then the expression cmc.location.x is well-formed, because the compiler can see the type of cmc.location.