|
1 | 1 | [](https://github.com/jkalias/sqlite-reflection/actions/workflows/cmake.yml) |
2 | 2 | [](https://github.com/jkalias/sqlite-reflection/blob/main/LICENSE) |
3 | | -# C++ SQLite with reflection |
| 3 | +# SQLite C++ frontend with reflection |
| 4 | + |
| 5 | +## Introduction |
| 6 | +A core feature of any application is persistence of user data, usually in a database. When there is no need for server storage, or even for fallback and/or backup reasons, SQLite offers a battle-tested and cross-platform solution for local database management. |
| 7 | + |
| 8 | +However, SQLite is written in C, and even though it exposes a [C++ API](https://www.sqlite.org/cintro.html), this tends to be rather verbose and full of details. In addition, the domain expert/programmer should always do manual bookeeping and cleanup of the relevant SQLite objects, so that no memory leaks occur. Finally, all operations are eventually expressed through raw SQL queries, which at the end is rather tedious and repetitive work. |
| 9 | + |
| 10 | +This library is inspired by the approach other languages and frameworks take against the problem of data persistence. Specifically, in C# the [Entity Framework](https://en.wikipedia.org/wiki/Entity_Framework) allows the programmer to focus on modelling the domain, while delegating the responsibility of database management, table allocation and naming, member type annotation and naming and so on, to EF. In Swift the feature of [keypaths](https://developer.apple.com/documentation/swift/keypath) allows the programmer to write safe code, which is checked at compile time. Its predecessor Objective-C has used keypaths extensively in the [Core Data](https://developer.apple.com/documentation/coredata) Framework, which is Apple's database management software stack, using primarily SQLite in the background. |
| 11 | + |
| 12 | +The primary goals of this library are |
| 13 | +* a native C++ API for object persistence, which feels "at home" for C++ programmers |
| 14 | +* safe code, checked at compile time, without the need to write raw SQL queries |
| 15 | +* automatic record registration for all types used in the program, without any additional setup |
| 16 | + |
| 17 | +## Detailed design |
| 18 | +### Creating a database object |
| 19 | +All database interactions are funneled through the database object. Before the database is accessed, it needs to know what record types it will operate on (more on that later), so it needs to be initialized. If you pass an empty string, an in-memory database will be created (useful for unit-testing). |
| 20 | +```c++ |
| 21 | +// initialization needs to happen at program startup |
| 22 | +#include "database.h" |
| 23 | + |
| 24 | +void MySetupCode() { |
| 25 | + std::string path_where_database_should_be_saved; |
| 26 | + ... |
| 27 | + Database::Initialize(path_where_database_should_be_saved); |
| 28 | + ... |
| 29 | +} |
| 30 | +``` |
| 31 | + |
| 32 | +Even though it's not strictly necessary, you are encouraged to finalize the database at program shutdown |
| 33 | +```c++ |
| 34 | +// good practice during program shutdown |
| 35 | +#include "database.h" |
| 36 | + |
| 37 | +void MyTearDownCode() { |
| 38 | + ... |
| 39 | + Database::Finalize(); |
| 40 | + ... |
| 41 | +} |
| 42 | +``` |
| 43 | + |
| 44 | +### Defining the record types and their members |
| 45 | +In order to define a type for persistence, just define its name and its members. For example, the following snippet declares a struct called `Person` and another struct called `Pet`. This is using the technique of [X Macro](https://en.wikipedia.org/wiki/X_Macro), which will prove out to be indispensable for automation of database operations, and it's the main facilitator of reflection in this library. |
| 46 | +```c++ |
| 47 | +// in Person.h |
| 48 | +#include <string> |
| 49 | + |
| 50 | +#define REFLECTABLE Person |
| 51 | +#define FIELDS \ |
| 52 | +MEMBER_TEXT(first_name) \ |
| 53 | +MEMBER_TEXT(last_name) \ |
| 54 | +MEMBER_INT(age) \ |
| 55 | +MEMBER_BOOL(is_vaccinated) \ |
| 56 | +FUNC(std::wstring GetFullName() const) |
| 57 | +#include "reflection.h" |
| 58 | + |
| 59 | +// either inline in the header or in a separate Person.cc file |
| 60 | +std::wstring Person::GetFullName() const { |
| 61 | + return first_name + L" " + last_name; |
| 62 | +} |
| 63 | + |
| 64 | +// equivalent to |
| 65 | +//struct Person { |
| 66 | +// std::wstring first_name; |
| 67 | +// std::wstring last_name; |
| 68 | +// int64_t age; |
| 69 | +// bool is_vaccinated; |
| 70 | +// int64_t id; <--- all records gain an id for unique identification in the database |
| 71 | +// |
| 72 | +// std::wstring GetFullName() const; |
| 73 | +//} |
| 74 | + |
| 75 | +// in Pet.h |
| 76 | +#include <string> |
| 77 | + |
| 78 | +#define REFLECTABLE Pet |
| 79 | +#define FIELDS \ |
| 80 | +MEMBER_TEXT(name) \ |
| 81 | +MEMBER_REAL(weight) |
| 82 | +#include "reflection.h" |
| 83 | + |
| 84 | +// equivalent to |
| 85 | +// struct Pet { |
| 86 | +// std::wstring name; |
| 87 | +// double weight; |
| 88 | +// int64_t id; <--- id for database |
| 89 | +//} |
| 90 | +``` |
| 91 | + |
| 92 | +During the database initialization phase, all record types (here `Person` and `Pet`) will be register in the database and the corresponding tables will be created if needed. No need for manual registration, no runtime errors due to forgotten records, which did not get registered. |
| 93 | + |
| 94 | +The following member attributes are allowed: |
| 95 | +* int64_t -> `MEMBER_INT` |
| 96 | +* double -> `MEMBER_REAL` |
| 97 | +* std::wstring -> `MEMBER_TEXT`. Wide strings are used in order to allow unicode strings to be saved in the database |
| 98 | +* int64_t -> `MEMBER_INT` |
| 99 | +* bool -> `MEMBER_BOOL` |
| 100 | +* custom functions -> `FUNC`. The corresponding function must be provided by the programmer. |
| 101 | + |
| 102 | +## Compilation (Cmake) |
| 103 | +### Dependencies |
| 104 | +* CMake >= 3.14 |
| 105 | + |
| 106 | +### Minimum C++ version |
| 107 | +* C++11 |
| 108 | + |
| 109 | +An out-of-source build strategy is used. All following examples assume an output build folder named ```build```. If no additional argument is passed to CMake, C++11 is used. Otherwise, you can pass ```-DCMAKE_CXX_STANDARD=20``` and it will use C++20 for example. |
| 110 | +### macOS (Xcode) |
| 111 | +```console |
| 112 | +cd sqlite-reflection |
| 113 | +cmake -S . -B build -G Xcode |
| 114 | +``` |
| 115 | +Then open the generated ```sqlite-reflection.xcodeproj``` in the ```build``` folder. |
| 116 | + |
| 117 | +### macOS (Makefiles/clang) |
| 118 | +```console |
| 119 | +cd sqlite-reflection |
| 120 | +cmake -S . -B build |
| 121 | +cmake --build build |
| 122 | +build/tests/unit_tests |
| 123 | +``` |
| 124 | + |
| 125 | +### macOS (Makefiles/g++) |
| 126 | +Assuming you have installed Homebrew, you can then use the gcc and g++ compilers by doing the following (this example uses version gcc 11) |
| 127 | +```console |
| 128 | +cd sqlite-reflection |
| 129 | +cmake \ |
| 130 | + -S . \ |
| 131 | + -B build \ |
| 132 | + -DCMAKE_C_COMPILER=/opt/homebrew/Cellar/gcc/11.2.0/bin/gcc-11 \ |
| 133 | + -DCMAKE_CXX_COMPILER=/opt/homebrew/Cellar/gcc/11.2.0/bin/g++-11 |
| 134 | +cmake --build build |
| 135 | +build/tests/unit_tests |
| 136 | +``` |
| 137 | + |
| 138 | +### Linux (Makefiles) |
| 139 | +```console |
| 140 | +cd sqlite-reflection |
| 141 | +cmake -S . -B build |
| 142 | +cmake --build build |
| 143 | +build/tests/unit_tests |
| 144 | +``` |
| 145 | + |
| 146 | +### Windows (Visual Studio) |
| 147 | +```console |
| 148 | +cd sqlite-reflection |
| 149 | +cmake -S . -B build |
| 150 | +``` |
| 151 | +Then open the generated ```sqlite-reflection.sln``` in the ```build``` folder. |
0 commit comments