You may generate SQL queries or YQL queries (for YDB) from .sql / .yql files. To do this, call the following cmake function in your CMakeLists.txt:
It will generate the samples_postgres_service/sql_queries.hpp file with following variable:
And the definition for each statement in samples_postgres_service/sql_queries.cpp looks something like that:
Each variable is statically initialized (has no dynamic|runtime initialization), giving a protection against static initialization order fiasco when those variables are used.
You may use it as usual by passing to storages::postgres::Cluster::Execute() or storages::clickhouse::Cluster for SQL files or ydb::TableClient::ExecuteDataQuery() for YQL files:
While writing tests, you can check the coverage of your SQL/YQL queries using the sql_coverage plugin.
To use it, you need to pass the target with generated queries to the userver_testsuite_add_simple (or userver_testsuite_add) function in your CMakeLists.txt as SQL_LIBRARY parameter:
It will enable the sql_coverage plugin and add coverage test that will run with the other tests.
Besides the raw storages::Query constants shown above, userver_add_sql_library can also generate a typed C++ client from your PostgreSQL migrations and .sql files. You get:
The typed client is added on top of the usual sql_queries.hpp / sql_queries.cpp by the same userver_add_sql_library call — just pass the DTO_DIALECT argument.
Add DTO_DIALECT, MIGRATIONS_DIR and DUMP_DIR to your userver_add_sql_library call:
Relevant arguments:
| Argument | Meaning |
|---|---|
| DTO_DIALECT | Enables DTO codegen for the given dialect. Currently only postgresql is supported. |
| MIGRATIONS_DIR | Directory with .sql migration files that define the schema. Required when DTO_DIALECT is set. |
| DUMP_DIR | Directory where the schema dump schema.dto.json is stored, relative to ${CMAKE_CURRENT_SOURCE_DIR}(defaults to .) |
The library target always links against userver::core; setting DTO_DIALECT additionally links it against userver::${DTO_DIALECT}.
Conventional input layout:
Each query file's stem becomes the method name in CamelCase: select_value.sql → SelectValue. The <group> subdirectory is ignored.
For NAMESPACE = <ns>:
All of them are compiled into the single library target defined by userver_add_sql_library.
To work out the C++ types, the generator needs the real PostgreSQL schema. A normal build does not start a database for this — it reads a committed dump (schema.dto.json) from DUMP_DIR. The dump stores a hash of your migrations and queries, so the generator can tell when it is out of date.
Once you change a migration or a query, the old dump no longer matches and the build fails. The error message prints the exact command to refresh the dump; running it starts a temporary PostgreSQL, reads the schema, and writes a fresh schema.dto.json to DUMP_DIR. So the loop is:
Commit schema.dto.json together with the change that caused it, so other developers and CI build without starting a database.
The examples below come from scripts/sqldto/tests/golden_tests/. Browse input/pg_queries/ and output/pg_queries/ for the full input/output pair.
Input — migrations/V001__queries_schema.sql (excerpt):
Generated pg_models.hpp:
Enum variants get kPascalCase names. Composite struct fields are always wrapped in std::optional<> because PostgreSQL composite-type fields are nullable.
Input — queries/get_user_by_id.sql:
Generated result struct (in pg_client.hpp) and the method on PgClient:
A query that returns more than one column gets a generated <Query>Row aggregate with one field per result column, named after the column. A query that returns a single column returns that column's type directly, with no wrapper struct.
arg1 is $1. The default cardinality is many, so the method returns a std::vector<...>. HostType is an alias for storages::postgres::ClusterHostTypeFlags — pass storages::postgres::ClusterHostType::kMaster or kSlave.
Input — queries/create_user.sql:
Generated method, returning a CreateUserRow struct (same fields as GetUserByIdRow above, since the RETURNING list is the same):
@arg1: TEXT and @arg2: TEXT pin the PostgreSQL types of $1, $2 to TEXT; $3 has no annotation and its type is inferred. Use @arg<N> when the generator cannot infer a parameter's type on its own (it fails the build asking you to annotate).
The annotation does not decide whether an argument is std::optional<>. That is a separate check: the generator tries passing NULL for each parameter and sees if PostgreSQL accepts it. Here $1 and $2 are INSERT values for NOT NULL columns, so they come out non-optional. The same value used only in a WHERE id = $1 clause would come out as std::optional<...>, even with a pinned type.
Build a ClusterPgClient from the cluster of a components::Postgres component and call its methods directly. From samples/postgres_dto_service:
In unit tests, depend on the PgClient interface instead and pass MockPgClient from pg_mock.hpp.
The generator emits the same C++ types used by the PostgreSQL driver. See uPg: Supported data types for the full semantics, including timestamps and arrays.
| PostgreSQL type | C++ type |
|---|---|
| boolean | bool |
| smallint, smallserial | std::int16_t |
| integer, serial | std::int32_t |
| bigint, bigserial | std::int64_t |
| real | float |
| double precision | double |
| numeric(p,s) / decimal(p,s) | decimal64::Decimal |
| text, varchar(n), character varying(n), character(n) | std::string |
| char | char |
| bytea | std::string |
| date | storages::postgres::Date |
| time, time without time zone | storages::postgres::TimeOfDay |
| time with time zone, timetz | storages::postgres::TimeOfDayTz |
| timestamp, timestamp without time zone | storages::postgres::TimePointWithoutTz |
| timestamp with time zone, timestamptz | storages::postgres::TimePointTz |
| interval | std::chrono::microseconds |
| json, jsonb | formats::json::Value |
| uuid | boost::uuids::uuid |
| int4range | storages::postgres::IntegerRange |
| int8range | storages::postgres::BigintRange |
| T[] | std::vector<T> |
| user CREATE TYPE ... AS ENUM | enum class (PascalCase; variants prefixed k) |
| user CREATE TYPE ... AS (...) | struct (PascalCase; all fields std::optional<>) |
Nullable result columns and parameters are wrapped in std::optional<T>.
Annotations are single-line SQL comments -- @<name>[: value] inside a .sql query file. At most one directive per comment line. Unknown directives cause codegen to fail.
| Annotation | Syntax | Default | Effect |
|---|---|---|---|
| @no-dto | -- @no-dto | DTO method is generated | Skip codegen for this query. The raw storages::Query constant is still produced; no PgClient method. Use it for queries you run only as a raw storages::Query, or that the analyzer cannot process (e.g. dynamic SQL, or statements it cannot prepare). |
| @cardinality: V | -- @cardinality: one\|optional\|many\|void | many | Shape of the return type, where RowType is the <Query>Row struct (multi-column) or the single column's type: one → RowType; optional → std::optional<RowType>; many → std::vector<RowType>. A query with no result columns always returns void. void is accepted but has no distinct effect — a query that does return columns is treated as many. |
| @arg<N>: <pg_type> | -- @arg1: TEXT | inferred from query | Pin the PostgreSQL type of $<N> (1-based). Use it when the type can't be inferred. It does not change whether the argument is std::optional<>. |
Examples: