...one of the most highly
regarded and expertly designed C++ library projects in the
world.
— Herb Sutter and Andrei
Alexandrescu, C++
Coding Standards
This section delves deeper on how to use field
and field_view
and its underlying types. Please make sure you've read this
section before going on.
This library implements two types representing the concept of a field: field_view
,
which is non-owning, and field
,
which is owning. The relationship between them is similar to the one between
std::string_view
and std::string
.
For efficiency reasons, all library functions return field_view
s.
For example:
results result; conn.query("SELECT 'Hello world!'", result); // fv doesn't own its memory; if result goes out of scope, fv becomes invalid field_view fv = result.rows().at(0).at(0); // sv also points into result; if result goes out of scope, sv becomes invalid string_view sv = fv.as_string();
When dealing with scalars (anything that is neither a string nor a blob),
field_view
's accessors make
a copy of the scalar:
results result; conn.query("SELECT 42", result); // fv doesn't own its memory; if result goes out of scope, fv becomes invalid field_view fv = result.rows().at(0).at(0); // intv is valid even after result goes out of scope std::int64_t intv = fv.as_int64();
field_view
s are cheap to
create and to copy, as they are small objects and don't perform any memory
allocations. They are also immutable. On the other hand, field
s
may be more expensive to create and copy, as they may perform memory allocations.
You may create a field
from
a field_view
, taking ownership
of its contents:
results result; conn.query("SELECT 'Hello world!'", result); // fv doesn't own its memory; if result goes out of scope, fv becomes invalid field_view fv = result.rows().at(0).at(0); // f takes ownership of fv's contents. f is valid even after result goes out of scope field f(fv);
field
and field_view
use the same underlying types for scalars. For strings and blobs, field
uses the owning types std::string
and blob
,
while field_view
uses the
reference types string_view
and blob_view
.
field
accessors return references,
which allow you to mutate the underlying object:
field f("my_string"); // constructs a field that owns the string "my_string" std::string& s = f.as_string(); // s points into f's storage s.push_back('2'); // f now holds "my_string2"
You can also mutate a field
using the assignment operator. This allows you to also change the underlying
type of a field
:
field f("my_string"); // constructs a field that owns the string "my_string" f = 42; // destroys "my_string" and stores the value 42 as an int64
This library uses std::int64_t
to represent all MySQL signed integers
(TINYINT
, SMALLINT
,
MEDIUMINT
...). Similarly,
unsigned integers use std::uint64_t
,
32-bit floats use float
and
64-bit doubles use double
.
String types (CHAR
, VARCHAR
, TEXT
,
SET
...) are represented as
string fields, which translates into string_view
for field_view
and std::string
for field
.
This library performs no character set conversion on strings. All strings
are provided as the server sends them. If you've issued a "SET
NAMES <charset-name>"
statement, strings will
be encoded according to <charset-name>
.
For details, see this section.
The JSON
type is represented
as a string containing the serialized JSON value. The DECIMAL
type is also provided as a string, to avoid losing precision.
The types BINARY
, VARBINARY
and BLOB
(of any size) are represented as blob fields, which are similar to string
fields, but represent binary data instead of characters. The following types
are used:
blob_view
is used by field_view
.
It's an alias for boost::span<const unsigned
char>
(a C++11 backport of std::span
).
blob
is used by field
. It's
an alias for std::vector<unsigned char>
.
If MySQL adds any other type that this library doesn't understand, it is also represented as a blob.
MySQL DATE
is represented as date
in C++. date
holds the year,
month and day components of a date. It is a type close to the protocol, rather
than a vocabulary type. The main reason for using date
instead of a std::chrono::time_point
type is that, under certain
configurations, MySQL allows storing invalid dates, such as 2020-00-01
. These
are not representable as a std::chrono::time_point
.
Unless dealing with these special values, we recommend converting date
s to a time_point
before using them. The member type date::time_point
is a system-clock time_point
that can represent the entire MySQL DATE
range without problems. You can use date::as_time_point
to perform the cast:
date d(2020, 2, 19); // d holds "2020-02-19" date::time_point tp = d.as_time_point(); // now use tp normally
If the date is not valid, as_time_point
will throw an exception.
You can query whether a date
contains a valid date or not using date::valid
:
date d1(2020, 2, 19); // regular date bool v1 = d1.valid(); // true date d2(2020, 0, 19); // invalid date bool v2 = d2.valid(); // false
You can combine it with date::get_time_point
,
which performs an unchecked conversion:
date d = /* obtain a date somehow */ date(2020, 2, 29); if (d.valid()) { // Same as as_time_point, but doesn't check for validity // Caution: be sure to check for validity. // If d is not valid, get_time_point results in undefined behavior date::time_point tp = d.get_time_point(); // Use tp as required std::cout << tp.time_since_epoch().count() << std::endl; } else { // the date is invalid std::cout << "Invalid date" << std::endl; }
You can also construct a date
from a time_point
. If the
time_point
is out of range,
an exception is thrown.
Both types are represented as datetime
in C++. datetime
represents
a broken time point, having year, month, day, hour, minute, second and microseconds.
MySQL also accepts invalid datetimes (like 2020-00-10 10:20:59.000000
).
The same considerations for date
apply:
datetime dt1(2020, 10, 11, 10, 20, 59, 123456); // regular datetime 2020-10-11 10:20:59.123456 bool v1 = dt1.valid(); // true datetime dt2(2020, 0, 11, 10, 20, 59); // invalid datetime 2020-00-10 10:20:59.000000 bool v2 = dt2.valid(); // false datetime::time_point tp = dt1.as_time_point(); // convert to time_point
The datetime
object doesn't
carry any time zone information with it. The time zone semantics depend on
the actual MySQL type:
DATETIME
is a "naive"
time point object. It represents a time point without any time zone information
at all. It is up to the user to interpret which time zone the object
is in.
TIMESTAMP
object is inserted,
it is interpreted to be in the connection's local time zone, as given
by the time_zone
variable, and converted
to UTC for storage. When retrieved, it is converted back to the time
zone indicated by time_zone
. The retrieved value
of a TIMESTAMP
field
is thus a time point in some local time zone, dictated by the current
time_zone
variable. As this
variable can be changed programmatically from SQL, without the library
knowing it, we represent TIMESTAMP
's
using the datetime
object,
which doesn't include time zone information.
When using TIMESTAMP
, we
recommend setting the time_zone
to a known value. To
illustrate this, consider an event-logging system with the following table
definition:
results result; conn.query( R"%( CREATE TEMPORARY TABLE events ( id INT PRIMARY KEY AUTO_INCREMENT, t TIMESTAMP, contents VARCHAR(256) ) )%", result );
We will be inserting events with an explicit timestamp. We may also want to retrieve events with a timestamp filter. This is what our prepared statements would look like:
auto insert_stmt = conn.prepare_statement("INSERT INTO events (t, contents) VALUES (?, ?)"); auto select_stmt = conn.prepare_statement("SELECT id, t, contents FROM events WHERE t > ?");
These statements may be run from different parts of our code, or even from
different applications. To get consistent results, we must make sure that
the time zones used during insertion and retrieval are the same. By default,
time_zone
gets set to SYSTEM
, which will use the server's time
zone settings. This is not what we want here, so let's change it:
// This change has session scope. All operations after this query // will now use UTC for TIMESTAMPs. Other sessions will not see the change. // If you need to reconnect the connection, you need to run this again. conn.query("SET @time_zone = 'UTC'", result);
With this, the insertion code can look like:
// Get the timestamp of the event. This may have been provided by an external system // For the sake of example, we will use the current timestamp datetime event_timestamp = datetime::now(); // event_timestamp will be interpreted as UTC if you have run SET @time_zone conn.execute_statement(insert_stmt, std::make_tuple(event_timestamp, "Something happened"), result);
The querying code would be:
// Get the timestamp threshold from the user. We will use a constant for the sake of example datetime threshold = datetime(2022, 1, 1); // get events that happened after 2022-01-01 // threshold will be interpreted as UTC. The retrieved events will have their // `t` column in UTC conn.execute_statement(select_stmt, std::make_tuple(threshold), result);
If you don't set time_zone
, you may apparently get
the right results if you run both insertions and queries from clients that
don't set time_zone
and the
server doesn't change its configuration. However, relying on this will make
your applications brittle, so we don't recommend it.
The TIME
type is a signed duration
with a resolution of one microsecond. It is represented using the time
type, an alias for a std::chrono::duration
specialization with microseconds as period.
This table summarizes
the available accesors for field_view
and field
.
The following table reflects mapping from database types to C++ types. The
range column shows the range of values that MySQL admits for that type. This
library guarantees that any field retrieved from the database honors that
range. The column_type
column
shows what metadata::type
would return for a column of that type.
MySQL type |
|
C++ type |
Range |
|
Considerations |
---|---|---|---|---|---|
|
|
|
|
1 byte integer |
|
|
|
|
|
|
1 byte integer |
|
|
|
|
2 byte integer |
|
|
|
|
|
|
2 byte integer |
|
|
|
|
3 byte integer |
|
|
|
|
|
|
3 byte integer |
|
|
|
|
4 byte integer |
|
|
|
|
|
|
4 byte integer |
|
|
|
|
8 byte integer |
|
|
|
|
|
|
8 byte integer |
|
|
[ |
|
1 byte integer type used to represent years Zero is often employed to represent invalid year values. We represent zero year as a numeric 0. |
|
|
|
Depends on the bitset width. Max |
|
A bitset between 1 and 64 bits wide. |
|
|
|
IEEE 754 |
|
4 byte floating point type |
|
|
|
IEEE 754 |
|
8 byte floating point type |
|
|
[ |
|
|||
|
[ |
|
Time point type without time zone, with a resolution of one microsecond. |
||
|
[ |
|
Time point type with a resolution of one microsecond. |
||
|
|
Signed time duration, with a resolution of one microsecond. |
|||
|
|
|
Fixed-size character string. |
||
|
|
|
Variable size character string with a maximum size. |
||
|
|
|
|
Variable size character string. |
|
|
|
|
Character string with a fixed set of possible values (only one possible). |
||
|
|
|
Character string with a fixed set of possible values (many possible). |
||
|
|
|
A serialized JSON value of any type.
Note that |
||
|
|
Depends on the column definition |
|
A fixed precision numeric value. In this case, the string will
contain the textual representation of the number (e.g. the string
|
|
|
|
Fixed-size blob. |
|||
|
|
Variable size blob with a maximum size. |
|||
|
|
|
Variable size blob. |
||
|
|
Any of the spatial data types. The string contains the binary representation of the geometry type. |
This section shows how a parameter v
in a expression conn.execute_statement(stmt, std::make_tuple(v),
result)
is interpreted by MySQL, depeding on v
's
type:
C++ type |
MySQL type |
Compatible with... |
---|---|---|
|
|
Signed |
|
|
Unsigned |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Any of the other types. Used to insert |
Depends on the actual type stored by the field |
||
Depends on the actual type stored by the field |