Skip to content

Commit 040070d

Browse files
authored
Update README.md
1 parent 10a6b9e commit 040070d

File tree

1 file changed

+130
-32
lines changed

1 file changed

+130
-32
lines changed

README.md

Lines changed: 130 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[![CMake Build Matrix](https://github.com/jkalias/sqlite-reflection/actions/workflows/cmake.yml/badge.svg)](https://github.com/jkalias/sqlite-reflection/actions/workflows/cmake.yml)
22
[![GitHub license](https://img.shields.io/github/license/jkalias/functional_cpp)](https://github.com/jkalias/sqlite-reflection/blob/main/LICENSE)
3-
# SQLite C++ frontend with reflection
3+
# C++ API for SQLite with compile time reflection
44

55
## Introduction
66
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.
@@ -13,36 +13,11 @@ The primary goals of this library are
1313
* a native C++ API for object persistence, which feels "at home" for C++ programmers
1414
* safe code, checked at compile time, without the need to write raw SQL queries
1515
* automatic record registration for all types used in the program, without any additional setup
16+
* a safe and easy API for all CRUD (Create, Read, Update, Delete) operations
1617

1718
## 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.
19+
### Model domain record types and their members
20+
In order to define a domain object for persistence, just define its name and its members. For example, the following snippet declares a struct called `Person` and another struct called `Pet`, which will both be saved in the database. 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.
4621
```c++
4722
// in Person.h
4823
#include <string>
@@ -89,16 +64,139 @@ MEMBER_REAL(weight)
8964
//}
9065
```
9166

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.
67+
During the database initialization phase, all record types (in the example `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.
9368

94-
The following member attributes are allowed:
69+
The following member attributes are allowed, based on the most commonly used SQLite column types:
9570
* int64_t -> `MEMBER_INT`
9671
* double -> `MEMBER_REAL`
97-
* std::wstring -> `MEMBER_TEXT`. Wide strings are used in order to allow unicode strings to be saved in the database
72+
* std::wstring -> `MEMBER_TEXT`. Wide strings are used in order to allow unicode text to be saved in the database.
9873
* int64_t -> `MEMBER_INT`
9974
* bool -> `MEMBER_BOOL`
75+
* timestamp -> `MEMBER_DATETIME` (read note below)
10076
* custom functions -> `FUNC`. The corresponding function must be provided by the programmer.
10177

78+
Special note for timestamps. Very often one needs to save a datetime (date with time) in the database for a given record type. C++ has an excellent `std::chrono` library to deal with time and duration, however the most useful features are available only in C++20 (and not guaranteed for all compiler vendors at the time of writing...) In order to facilitate a cross-platform solution which works all the way down to C++11, all datetimes are stored in their UTC [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) representation, by leveraging the (awesome) [date](https://github.com/HowardHinnant/date) library of Howard Hinnant, one of the main actors behind `std::chrono`.
79+
80+
### Creating a database object
81+
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 (as defined above), so it needs to be initialized. If you pass an empty string, an in-memory database will be created (useful for unit-testing).
82+
```c++
83+
// initialization needs to happen at program startup
84+
#include "database.h"
85+
86+
void MySetupCode(const std::string& db_path) {
87+
...
88+
Database::Initialize(db_path);
89+
...
90+
}
91+
```
92+
93+
Even though it's not strictly necessary, you are encouraged to finalize the database at program shutdown
94+
```c++
95+
// good practice during program shutdown
96+
#include "database.h"
97+
98+
void MyTearDownCode() {
99+
...
100+
Database::Finalize();
101+
...
102+
}
103+
```
104+
### Persist records (Save)
105+
In order to save objects in the database, you first need to get a hold of the database object and then pass it the records for persistence. You don't _have_ to pass multiple records, you can only use one if you need to.
106+
```c++
107+
const auto& db = Database::Instance();
108+
std::vector<Person> persons;
109+
110+
// id is here given - 5
111+
persons.push_back({L"peter", L"meier", 32, true, 5});
112+
... // add more records for persistence
113+
114+
db.Save(persons);
115+
116+
// if you don't want to manage the id yourself, just let the database manage it
117+
// leave the last argument empty (it's always the id)
118+
// persons.push_back({L"peter", L"meier", 32, true});
119+
120+
// this will set the record id to the next available value
121+
// db.SaveAutoIncrement(persons);
122+
```
123+
### Retrieve records (Read)
124+
In order to fetch records of a given type from the database, you first need to get a hold of the database object and then call a variant of the `Fetch` operation.
125+
```c++
126+
// assume that these persons have been previously saved in the database
127+
// {L"name1", L"surname1", 13, false, 1}
128+
// {L"john", L"surname2", 25, false, 2}
129+
// {L"john", L"surname3", 37, false, 3}
130+
// {L"jame", L"surname4", 45, false, 4}
131+
// {L"name5", L"surname5", 56, false, 5}
132+
133+
const auto& db = Database::Instance();
134+
135+
// retrieve all persons stored
136+
const auto all_persons = db.FetchAll<Person>();
137+
138+
// retrieve a person from a given id
139+
const auto specific_person = db.Fetch<Person>(5);
140+
141+
// create a custom predicate
142+
const auto fetch_condition = GreaterThanOrEqual(&Person::id, 2)
143+
.And(SmallerThan(&Person::id, 5))
144+
.And(Equal(&Person::first_name, L"john"));
145+
146+
// retrieve persons with custom predicate
147+
// this will fetch only
148+
// Person{L"john", L"surname2", 25, false, 2}
149+
// and
150+
// Person{L"john", L"surname3", 37, false, 3}
151+
const auto fetched_persons_with_predicate = db.Fetch<Person>(&fetch_condition);
152+
```
153+
154+
### Update records
155+
Updating records couldn't be simpler: just manipulate the needed members of the given records, and ship them back to the database for update.
156+
```c++
157+
// assume that these persons have been previously saved in the database
158+
// {L"john", L"doe", 28, false, 3}
159+
// {L"mary", L"poppins", 29, false, 5}
160+
161+
const auto& db = Database::Instance();
162+
163+
// retrieve all records
164+
std::vector<Person> persons = db.FetchAll<Person>();
165+
166+
// update the records as needed
167+
persons[0].last_name = L"rambo";
168+
persons[1].age = 20;
169+
170+
// update the records in the database
171+
db.Update(persons);
172+
```
173+
174+
### Delete records
175+
Deleting records can be done in three variants: with a given id, by passing the whole record, or by a custom predicate.
176+
```c++
177+
// assume that these persons have been previously saved in the database
178+
// {L"παναγιώτης", L"ανδριανόπουλος", 28, true, 3}
179+
// {L"peter", L"meier", 32, false, 5}
180+
// {L"mary", L"poppins", 20, true, 13}
181+
182+
const auto& db = Database::Instance();
183+
184+
const auto age_match_predicate = SmallerThan(&Person::age, 30)
185+
.And(Equal(&Person::is_vaccinated, true));
186+
187+
// this will leave only {L"peter", L"meier", 32, false, 5} in the database
188+
db.Delete<Person>(&age_match_predicate);
189+
190+
// this would delete the 3rd record entry of the vector
191+
// std::vector<Person> persons = db.FetchAll<Person>();
192+
// db.Delete(persons[2]);
193+
194+
// this would delete the record entry from its id
195+
// db.Delete<Person>(persons[1].id);
196+
// or
197+
// db.Delete<Person>(5);
198+
```
199+
102200
## Compilation (Cmake)
103201
### Dependencies
104202
* CMake >= 3.14

0 commit comments

Comments
 (0)