Example app in C++
This page contains a detailed description of the code of a test app that is available as part of the YDB C++SDK.
Initializing a database connection
To interact with YDB, create instances of the driver, client, and session:
- The YDB driver facilitates interaction between the app and YDB nodes at the transport layer. It must be initialized before creating a client or session and must persist throughout the YDB access lifecycle.
- The YDB client operates on top of the YDB driver and enables the handling of entities and transactions.
- The YDB session, which is part of the YDB client context, contains information about executed transactions and prepared queries.
App code snippet for driver initialization:
auto connectionParams = TConnectionsParams()
.SetEndpoint(endpoint)
.SetDatabase(database)
.SetAuthToken(GetEnv("YDB_TOKEN"));
TDriver driver(connectionParams);
App code snippet for creating a client:
TClient client(driver);
Creating tables
Create tables to be used in operations on a test app. This step results in the creation of database tables for the series directory data model:
Series
Seasons
Episodes
After the tables are created, a method for retrieving information about data schema objects is called, and the result of its execution is displayed.
//! Creates sample tables with the ExecuteQuery method
ThrowOnError(client.RetryQuerySync([](TSession session) {
auto query = Sprintf(R"(
CREATE TABLE series (
series_id Uint64,
title Utf8,
series_info Utf8,
release_date Uint64,
PRIMARY KEY (series_id)
);
)");
return session.ExecuteQuery(query, TTxControl::NoTx()).GetValueSync();
}));
Adding data
Add data to the created tables using the UPSERT
statement in YQL. A data update request is sent to the server as a single request with transaction auto-commit mode enabled.
Code snippet for data insert/update:
//! Shows basic usage of mutating operations.
void UpsertSimple(TQueryClient client) {
ThrowOnError(client.RetryQuerySync([](TSession session) {
auto query = Sprintf(R"(
UPSERT INTO episodes (series_id, season_id, episode_id, title) VALUES
(2, 6, 1, "TBD");
)");
return session.ExecuteQuery(query,
TTxControl::BeginTx(TTxSettings::SerializableRW()).CommitTx()).GetValueSync();
}));
}
PRAGMA TablePathPrefix
adds a specified prefix to the table paths. It uses standard filesystem path concatenation, meaning it supports parent folder referencing and does not require a trailing slash. For example:
PRAGMA TablePathPrefix = "/cluster/database";
SELECT * FROM episodes;
For more information about PRAGMA
in YQL, refer to the YQL documentation.
Retrieving data
Retrieve data using a SELECT
statement in YQL. Handle the retrieved data selection in the app.
To execute YQL queries, use the ExecuteQuery
method.
The SDK lets you explicitly control the execution of transactions and configure the transaction execution mode using the TTxControl
class.
In the code snippet below, the transaction settings are defined using the TTxControl::BeginTx
method. With TTxSettings
, set the SerializableRW
transaction execution mode. When all the queries in the transaction are completed, the transaction is automatically completed by explicitly setting CommitTx()
. The query
described using the YQL syntax is passed to the ExecuteQuery
method for execution.
void SelectSimple(TQueryClient client) {
TMaybe<TResultSet> resultSet;
ThrowOnError(client.RetryQuerySync([&resultSet](TSession session) {
auto query = Sprintf(R"(
SELECT series_id, title, CAST(release_date AS Date) AS release_date
FROM series
WHERE series_id = 1;
)");
auto txControl =
// Begin a new transaction with SerializableRW mode
TTxControl::BeginTx(TTxSettings::SerializableRW())
// Commit the transaction at the end of the query
.CommitTx();
auto result = session.ExecuteQuery(query, txControl).GetValueSync();
if (!result.IsSuccess()) {
return result;
}
resultSet = result.GetResultSet(0);
return result;
}));
Processing execution results
The TResultSetParser
class is used for processing query execution results.
The code snippet below shows how to process query results using the parser
object:
TResultSetParser parser(*resultSet);
while (parser.TryNextRow()) {
Cout << "> SelectSimple:" << Endl << "Series"
<< ", Id: " << parser.ColumnParser("series_id").GetOptionalUint64()
<< ", Title: " << parser.ColumnParser("title").GetOptionalUtf8()
<< ", Release date: " << parser.ColumnParser("release_date").GetOptionalDate()->FormatLocalTime("%Y-%m-%d")
<< Endl;
}
}
The given code snippet prints the following text to the console at startup:
> SelectSimple:
series, Id: 1, title: IT Crowd, Release date: 2006-02-03
Parameterized queries
Query data using parameters. This query execution method is preferable because it allows the server to reuse the query execution plan for subsequent calls and protects against vulnerabilities such as SQL injection.
The code snippet shows the use of parameterized queries and the TParamsBuilder
to generate parameters and pass them to the ExecuteQuery
method:
void SelectWithParams(TQueryClient client) {
TMaybe<TResultSet> resultSet;
ThrowOnError(client.RetryQuerySync([&resultSet](TSession session) {
ui64 seriesId = 2;
ui64 seasonId = 3;
auto query = Sprintf(R"(
DECLARE $seriesId AS Uint64;
DECLARE $seasonId AS Uint64;
SELECT sa.title AS season_title, sr.title AS series_title
FROM seasons AS sa
INNER JOIN series AS sr
ON sa.series_id = sr.series_id
WHERE sa.series_id = $seriesId AND sa.season_id = $seasonId;
)");
auto params = TParamsBuilder()
.AddParam("$seriesId")
.Uint64(seriesId)
.Build()
.AddParam("$seasonId")
.Uint64(seasonId)
.Build()
.Build();
auto result = session.ExecuteQuery(
query,
TTxControl::BeginTx(TTxSettings::SerializableRW()).CommitTx(),
params).GetValueSync();
if (!result.IsSuccess()) {
return result;
}
resultSet = result.GetResultSet(0);
return result;
}));
TResultSetParser parser(*resultSet);
if (parser.TryNextRow()) {
Cout << "> SelectWithParams:" << Endl << "Season"
<< ", Title: " << parser.ColumnParser("season_title").GetOptionalUtf8()
<< ", Series title: " << parser.ColumnParser("series_title").GetOptionalUtf8()
<< Endl;
}
}
The given code snippet prints the following text to the console at startup:
> SelectWithParams:
Season, title: Season 3, series title: Silicon Valley
Stream queries
Making a stream query that results in a data stream. Streaming lets you read an unlimited number of rows and amount of data.
Warning
Do not use the StreamExecuteQuery
method without wrapping the call with RetryQuery
or RetryQuerySync
.
void StreamQuerySelect(TQueryClient client) {
Cout << "> StreamQuery:" << Endl;
ThrowOnError(client.RetryQuerySync([](TQueryClient client) -> TStatus {
auto query = Sprintf(R"(
DECLARE $series AS List<UInt64>;
SELECT series_id, season_id, title, CAST(first_aired AS Date) AS first_aired
FROM seasons
WHERE series_id IN $series
ORDER BY season_id;
)");
auto paramsBuilder = TParamsBuilder();
auto& listParams = paramsBuilder
.AddParam("$series")
.BeginList();
for (auto x : {1, 10}) {
listParams.AddListItem().Uint64(x);
}
auto parameters = listParams
.EndList()
.Build()
.Build();
// Executes stream query
auto resultStreamQuery = client.StreamExecuteQuery(query, TTxControl::NoTx(), parameters).GetValueSync();
if (!resultStreamQuery.IsSuccess()) {
return resultStreamQuery;
}
// Iterates over results
bool eos = false;
while (!eos) {
auto streamPart = resultStreamQuery.ReadNext().ExtractValueSync();
if (!streamPart.IsSuccess()) {
eos = true;
if (!streamPart.EOS()) {
return streamPart;
}
continue;
}
// It is possible for lines to be duplicated in the output stream due to an external retrier
if (streamPart.HasResultSet()) {
auto rs = streamPart.ExtractResultSet();
TResultSetParser parser(rs);
while (parser.TryNextRow()) {
Cout << "Season"
<< ", SeriesId: " << parser.ColumnParser("series_id").GetOptionalUint64()
<< ", SeasonId: " << parser.ColumnParser("season_id").GetOptionalUint64()
<< ", Title: " << parser.ColumnParser("title").GetOptionalUtf8()
<< ", Air date: " << parser.ColumnParser("first_aired").GetOptionalDate()->FormatLocalTime("%Y-%m-%d")
<< Endl;
}
}
}
return TStatus(EStatus::SUCCESS, NYql::TIssues());
}));
}
The given code snippet prints the following text to the console at startup (there may be duplicate lines in the output stream due to an external RetryQuerySync
):
> StreamQuery:
Season, SeriesId: 1, SeasonId: 1, Title: Season 1, Air date: 2006-02-03
Season, SeriesId: 1, SeasonId: 2, Title: Season 2, Air date: 2007-08-24
Season, SeriesId: 1, SeasonId: 3, Title: Season 3, Air date: 2008-11-21
Season, SeriesId: 1, SeasonId: 4, Title: Season 4, Air date: 2010-06-25
Multistep transactions
Multiple statements can be executed within a single multistep transaction. Client-side code can run between query steps. Using a transaction ensures that queries executed in its context are consistent with each other.
The first step is to prepare and execute the first query:
//! Shows usage of transactions consisting of multiple data queries with client logic between them.
void MultiStep(TQueryClient client) {
TMaybe<TResultSet> resultSet;
ThrowOnError(client.RetryQuerySync([&resultSet](TSession session) {
ui64 seriesId = 2;
ui64 seasonId = 5;
auto query1 = Sprintf(R"(
DECLARE $seriesId AS Uint64;
DECLARE $seasonId AS Uint64;
SELECT first_aired AS from_date FROM seasons
WHERE series_id = $seriesId AND season_id = $seasonId;
)");
auto params1 = TParamsBuilder()
.AddParam("$seriesId")
.Uint64(seriesId)
.Build()
.AddParam("$seasonId")
.Uint64(seasonId)
.Build()
.Build();
// Execute the first query to retrieve the required values for the client.
// Transaction control settings do not set the CommitTx flag, allowing the transaction to remain active
// after query execution.
auto result = session.ExecuteQuery(
query1,
TTxControl::BeginTx(TTxSettings::SerializableRW()),
params1);
auto resultValue = result.GetValueSync();
if (!resultValue.IsSuccess()) {
return resultValue;
}
A transaction identifier needs to be obtained to continue working within the current transaction:
// Get the active transaction id
auto txId = resultValue.GetTransaction()->GetId();
// Processing the request result
TResultSetParser parser(resultValue.GetResultSet(0));
parser.TryNextRow();
auto date = parser.ColumnParser("from_date").GetOptionalUint64();
// Perform some client logic on returned values
auto userFunc = [] (const TInstant fromDate) {
return fromDate + TDuration::Days(15);
};
TInstant fromDate = TInstant::Days(*date);
TInstant toDate = userFunc(fromDate);
The next step is to create the next query that uses the results of code execution on the client side:
// Construct next query based on the results of client logic
auto query2 = Sprintf(R"(
DECLARE $seriesId AS Uint64;
DECLARE $fromDate AS Uint64;
DECLARE $toDate AS Uint64;
SELECT season_id, episode_id, title, air_date FROM episodes
WHERE series_id = $seriesId AND air_date >= $fromDate AND air_date <= $toDate;
)");
auto params2 = TParamsBuilder()
.AddParam("$seriesId")
.Uint64(seriesId)
.Build()
.AddParam("$fromDate")
.Uint64(fromDate.Days())
.Build()
.AddParam("$toDate")
.Uint64(toDate.Days())
.Build()
.Build();
// Execute the second query.
// The transaction control settings continue the active transaction (tx)
// and commit it at the end of the second query execution.
auto result2 = session.ExecuteQuery(
query2,
TTxControl::Tx(txId).CommitTx(),
params2).GetValueSync();
if (!result2.IsSuccess()) {
return result2;
}
resultSet = result2.GetResultSet(0);
return result2;
})); // The end of the retried lambda
TResultSetParser parser(*resultSet);
Cout << "> MultiStep:" << Endl;
while (parser.TryNextRow()) {
auto airDate = TInstant::Days(*parser.ColumnParser("air_date").GetOptionalUint64());
Cout << "Episode " << parser.ColumnParser("episode_id").GetOptionalUint64()
<< ", Season: " << parser.ColumnParser("season_id").GetOptionalUint64()
<< ", Title: " << parser.ColumnParser("title").GetOptionalUtf8()
<< ", Air date: " << airDate.FormatLocalTime("%a %b %d, %Y")
<< Endl;
}
}
The given code snippets output the following text to the console at startup:
> MultiStep:
Episode 1, Season: 5, title: Grow Fast or Die Slow, Air date: Sun Mar 25, 2018
Episode 2, Season: 5, title: Reorientation, Air date: Sun Apr 01, 2018
Episode 3, Season: 5, title: Chief Operating Officer, Air date: Sun Apr 08, 2018
Managing transactions
Transactions are managed through TCL Begin
and Commit
calls.
In most cases, instead of explicitly using Begin
and Commit
calls, it's better to use transaction control parameters in execute calls. This allows to avoid additional requests to YDB server and thus run queries more efficiently.
Code snippet for BeginTransaction
and tx.Commit()
calls:
void ExplicitTcl(TQueryClient client) {
// Demonstrate the use of explicit Begin and Commit transaction control calls.
// In most cases, it's preferable to use transaction control settings within ExecuteDataQuery calls instead,
// as this avoids additional hops to the YDB cluster and allows for more efficient query execution.
ThrowOnError(client.RetryQuerySync([](TQueryClient client) -> TStatus {
auto airDate = TInstant::Now();
auto session = client.GetSession().GetValueSync().GetSession();
auto beginResult = session.BeginTransaction(TTxSettings::SerializableRW()).GetValueSync();
if (!beginResult.IsSuccess()) {
return beginResult;
}
// Get newly created transaction id
auto tx = beginResult.GetTransaction();
auto query = Sprintf(R"(
DECLARE $airDate AS Date;
UPDATE episodes SET air_date = CAST($airDate AS Uint16) WHERE title = "TBD";
)");
auto params = TParamsBuilder()
.AddParam("$airDate")
.Date(airDate)
.Build()
.Build();
// Execute query.
// Transaction control settings continues active transaction (tx)
auto updateResult = session.ExecuteQuery(query,
TTxControl::Tx(tx.GetId()),
params).GetValueSync();
if (!updateResult.IsSuccess()) {
return updateResult;
}
// Commit active transaction (tx)
return tx.Commit().GetValueSync();
}));
}