|
Lightweight 0.20260625.0
|
Lightweight is a modern C++23 database library for Microsoft SQL Server, PostgreSQL and SQLite over ODBC — with a data mapper and typed relationships, versioned schema migrations, parallel backup and restore, an async coroutine API, connection pooling, and CLI/GUI tooling.
It is layered: use the thin ODBC wrapper (SqlConnection, SqlStatement) when you want raw SQL and full control, the query builder when you want composable typed SQL, or the DataMapper when you want records and relationships mapped for you. The layers interoperate — you can drop from one to the next at any point without leaving the library.
Documentation is available at https://lastrada-software.github.io/Lightweight/.
| Area | What it gives you | Guide |
|---|---|---|
| Raw SQL access | SqlConnection, SqlStatement, prepared statements, batched execution, block-prefetch | usage.md |
| Query builder | Composable typed SELECT/INSERT/UPDATE/DELETE, joins, filtering, ordering, pagination | sqlquery.md |
| Data mapper | Struct-to-table mapping, CRUD, Field<> with primary keys and nullability | usage.md |
| Relationships | BelongsTo, HasMany, HasOneThrough, HasManyThrough, CompositeForeignKey | composite-keys-design.md |
| Schema migrations | Versioned migrations, checksums, dependency ordering, plugin loading, rollback | sql-migrations.md |
| Backup & restore | Parallel chunked dump/restore, msgpack + zip + sha256, archive diffing | sql-backup.md, sql-backup-format.md |
| Async API | C++23 coroutines: Task<T>, executors, strand, stdexec bridge, async DataMapper | async.md |
| Connection pooling | Compile-time-configured pool, async-aware, recycles connections across mappers | async.md |
| Logging & tracing | Pluggable SqlLogger: warnings/errors, full SQL trace, or your own sink | logging.md |
| Schema introspection | Read tables, columns, keys and indexes back out of a live database | schema-introspection.md |
| Custom data types | SqlDataBinder<T> specialization for your own types, with Unicode support | data-binder.md |
**dbtool CLI** | Migrations, backup/restore, backup diffing, schema inspection — ~20 commands | dbtool.md |
**dbtool-gui** | Qt/QML desktop app for migrations, backup and ad-hoc queries | dbtool.md |
**ddl2cpp** | Generates C++ records from an existing schema, inferring relations automatically | ddl2cpp-relation-generation.md |
Coming from SQL? sql-to-lightweight.md is a side-by-side cookbook that shows the Lightweight equivalent of a given piece of SQL in each of the three layers. See also best-practices.md and how-to.md.
ODBC is a deliberate trade-off, not an accident of history. Every other ecosystem — and within C++ every ergonomic peer (sqlpp23, sqlgen, ormpp, sqlite_orm, libpqxx, Drogon) — talks native wire protocols per database. Lightweight targets ODBC because it buys one API, one build, and one set of semantics across SQL Server, PostgreSQL and SQLite, with per-database differences funnelled through a single dispatch point (SqlQueryFormatter) rather than scattered across the codebase. For applications that must ship against more than one database, that is worth a great deal.
What it costs, stated plainly:
COPY-class fast path; bulk work goes through array/parameter binding, which is fast but not as fast as a native bulk loader.Only ODBC is supported, so it should work on any platform that has an ODBC driver and a modern enough C++ compiler.
SqlServerType also lists a MYSQL enumerator, but MySQL is not supported: SqlQueryFormatter::Get() returns nullptr for it, so no query can be formatted. The enumerator is a placeholder for possible future work — do not rely on it.
Being explicit here saves you from discovering these by reading the source.
HAVING, CTEs, UNION/EXCEPT/INTERSECT, RETURNING, upsert, window functions, or multi-row INSERT ... VALUES (bulk goes through array binding instead). The intended escape hatches are SqlFieldExpression, WhereRaw(...), and DataMapper::Query<T>(sql, ...) — reach for them when you need SQL the builder does not model. For RETURNING specifically, SqlStatement can execute it directly, with driver caveats documented in how-to.md.BelongsTo holds a deep copy; HasMany builds fresh shared_ptrs on every load.DataMapper::AcquireThreadLocal(), which is constructed from SqlConnection::DefaultConnectionString(). A lazy load therefore runs on a different connection, outside your caller's transaction, and fails outright if no default connection string is configured. If that matters to your code, load relations explicitly instead of touching them lazily.All functionality is placed inside a Lightweight namespace, we also provide an alias for this namespace Light, that is slightly shorter.
High level API of the library provided by the type DataMapper
Example of its usage to save/load/update/delete entry in the database for one table
Now consider the following example we have two tables User and Email, with foreign key in Email pointing to the User this will translate in the following structs
BelongsTo models the many-to-one side of a foreign key — the record that owns the foreign-key column. Many Emails can point at one User. For the other relationship kinds see HasMany, HasOneThrough, HasManyThrough and CompositeForeignKey.
In the presented example we used rename of the columns, for more details see how-to#rename-column-name page. you can query the email and get access to the user record as well
Note: lazy loading (
email.user->nameabove) does not usedm. It uses a thread-localDataMapperbuilt from the default connection string, so it runs on a different connection and outside any transactiondmmay be in. See Known limitations.
If you have a SQL query that returns some values, but it does not correspond to an existing table in the database, you can map the result to a simple struct. The struct must have fields that match the columns in the query. The fields can be of any type that can be converted from the column type. The struct can have more fields than the columns in the query, but the fields that match the columns must be in the same order as the columns in the query.
We also provide an API to create SQL queries, this can be useful if you want to use information from existing structures. The following example shows how to create a query that joins multiple tables and maps the result to multiple structs. Consider the following structs
Create a query to join those tables to get in a single query
This create the following SQL query
Now you can execute it and get the result as a std::vector<std::tuple<CustomBindingA, CustomBindingB, PartOfC>> like this
Migrations are versioned, checksummed C++ definitions applied in dependency order, with rollback and plugin loading. They can be driven from your application or from the dbtool CLI:
See sql-migrations.md for writing migrations and dbtool.md for the full command reference.
Lightweight ships a backup engine — parallel chunked dump and restore into a zip archive of msgpack chunks with sha256 integrity, plus archive diffing:
Backups are taken online, without a snapshot, so there is no cross-table consistency guarantee — see Known limitations. Details in sql-backup.md, archive layout in sql-backup-format.md.
Async entry points are added directly to the types you already use (SqlConnection, DataMapper, Pool), suffixed with Async, and return Async::Task<T>. Enable it once by saying where blocking ODBC calls run and where your coroutine resumes:
Queries then go through the same fluent builder — start the chain with QueryAsync<Record>() instead of Query<Record>(), and every finisher returns a Task of its usual result:
Two things to know before you build on this. It is thread-offload, not protocol-level async: your application thread never blocks, but a worker thread does. And the async operands are captured by reference, so keep the whole expression inside the co_await — hoisting a builder into a local and awaiting it later is a use-after-free.
async.md covers executors, cancellation, transactions, single- versus multi-threaded drive models, and the std::execution bridge.
You need to have the SQLite3 ODBC driver for SQLite installed.
"DRIVER={SQLite3 ODBC Driver};Database=file::memory:""DRIVER=SQLite3;Database=file::memory:"You can use ddl2cpp to generate header file for you database schema as well as an example file that you can compile
First, configure cmake project and compile ddl2cpp target
Generate header file from the existing database by providing connection string to the tool
You can also avoid all those command line arguments by creating a config file that must be in your current working directory or in one of its parent directories. The config file must be named ddl2cpp.yml and must contain the following content:
Now you can configure cmake to compile example
Finally, compile and run the example
ddl2cpp also infers relationships from the schema's foreign keys — see ddl2cpp-relation-generation.md.
Lightweight supports building as C++20 modules. To enable this feature, you need CMake 3.28 or higher.
Enable module support with the LIGHTWEIGHT_BUILD_MODULES CMake option:
When modules are enabled, consumers can import the library using:
Note: C++20 module support requires: