Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions .mapping.json
Original file line number Diff line number Diff line change
Expand Up @@ -3640,14 +3640,23 @@
"odbc/functional_tests/basic_chaos/schemas/postgresql/key_value.sql":"taxi/uservices/userver/odbc/functional_tests/basic_chaos/schemas/postgresql/key_value.sql",
"odbc/functional_tests/basic_chaos/static_config.yaml":"taxi/uservices/userver/odbc/functional_tests/basic_chaos/static_config.yaml",
"odbc/functional_tests/basic_chaos/tests/conftest.py":"taxi/uservices/userver/odbc/functional_tests/basic_chaos/tests/conftest.py",
"odbc/functional_tests/basic_chaos/tests/test_metrics.py":"taxi/uservices/userver/odbc/functional_tests/basic_chaos/tests/test_metrics.py",
"odbc/functional_tests/basic_chaos/tests/test_odbc.py":"taxi/uservices/userver/odbc/functional_tests/basic_chaos/tests/test_odbc.py",
"odbc/functional_tests/secdist_update/CMakeLists.txt":"taxi/uservices/userver/odbc/functional_tests/secdist_update/CMakeLists.txt",
"odbc/functional_tests/secdist_update/odbc_service.cpp":"taxi/uservices/userver/odbc/functional_tests/secdist_update/odbc_service.cpp",
"odbc/functional_tests/secdist_update/schemas/postgresql/key_value.sql":"taxi/uservices/userver/odbc/functional_tests/secdist_update/schemas/postgresql/key_value.sql",
"odbc/functional_tests/secdist_update/static_config.yaml":"taxi/uservices/userver/odbc/functional_tests/secdist_update/static_config.yaml",
"odbc/functional_tests/secdist_update/tests/conftest.py":"taxi/uservices/userver/odbc/functional_tests/secdist_update/tests/conftest.py",
"odbc/functional_tests/secdist_update/tests/test_secdist.py":"taxi/uservices/userver/odbc/functional_tests/secdist_update/tests/test_secdist.py",
"odbc/include/userver/storages/odbc.hpp":"taxi/uservices/userver/odbc/include/userver/storages/odbc.hpp",
"odbc/include/userver/storages/odbc/cluster.hpp":"taxi/uservices/userver/odbc/include/userver/storages/odbc/cluster.hpp",
"odbc/include/userver/storages/odbc/cluster_types.hpp":"taxi/uservices/userver/odbc/include/userver/storages/odbc/cluster_types.hpp",
"odbc/include/userver/storages/odbc/command_control.hpp":"taxi/uservices/userver/odbc/include/userver/storages/odbc/command_control.hpp",
"odbc/include/userver/storages/odbc/component.hpp":"taxi/uservices/userver/odbc/include/userver/storages/odbc/component.hpp",
"odbc/include/userver/storages/odbc/exception.hpp":"taxi/uservices/userver/odbc/include/userver/storages/odbc/exception.hpp",
"odbc/include/userver/storages/odbc/execution_result.hpp":"taxi/uservices/userver/odbc/include/userver/storages/odbc/execution_result.hpp",
"odbc/include/userver/storages/odbc/field.hpp":"taxi/uservices/userver/odbc/include/userver/storages/odbc/field.hpp",
"odbc/include/userver/storages/odbc/impl/parameter.hpp":"taxi/uservices/userver/odbc/include/userver/storages/odbc/impl/parameter.hpp",
"odbc/include/userver/storages/odbc/impl/tracing_tags.hpp":"taxi/uservices/userver/odbc/include/userver/storages/odbc/impl/tracing_tags.hpp",
"odbc/include/userver/storages/odbc/odbc_fwd.hpp":"taxi/uservices/userver/odbc/include/userver/storages/odbc/odbc_fwd.hpp",
"odbc/include/userver/storages/odbc/query.hpp":"taxi/uservices/userver/odbc/include/userver/storages/odbc/query.hpp",
Expand Down Expand Up @@ -3691,7 +3700,6 @@
"odbc/src/storages/odbc/dsn.hpp":"taxi/uservices/userver/odbc/src/storages/odbc/dsn.hpp",
"odbc/src/storages/odbc/exception.cpp":"taxi/uservices/userver/odbc/src/storages/odbc/exception.cpp",
"odbc/src/storages/odbc/field.cpp":"taxi/uservices/userver/odbc/src/storages/odbc/field.cpp",
"odbc/src/storages/odbc/odbc_config.hpp":"taxi/uservices/userver/odbc/src/storages/odbc/odbc_config.hpp",
"odbc/src/storages/odbc/odbc_secdist.cpp":"taxi/uservices/userver/odbc/src/storages/odbc/odbc_secdist.cpp",
"odbc/src/storages/odbc/odbc_secdist.hpp":"taxi/uservices/userver/odbc/src/storages/odbc/odbc_secdist.hpp",
"odbc/src/storages/odbc/result_set.cpp":"taxi/uservices/userver/odbc/src/storages/odbc/result_set.cpp",
Expand Down Expand Up @@ -6670,4 +6678,4 @@
"ydb/utest/include/userver/ydb/tests/topic_writer_mock.hpp":"taxi/uservices/userver/ydb/utest/include/userver/ydb/tests/topic_writer_mock.hpp",
"ydb/utest/include/userver/ydb/tests/write_session_mock.hpp":"taxi/uservices/userver/ydb/utest/include/userver/ydb/tests/write_session_mock.hpp",
"ydb/utest/src/utest/topic_writer_mock.cpp":"taxi/uservices/userver/ydb/utest/src/utest/topic_writer_mock.cpp"
}
}
231 changes: 3 additions & 228 deletions odbc/README.md
Original file line number Diff line number Diff line change
@@ -1,229 +1,4 @@
# userver: ODBC Driver Wrapper [WIP]

ODBC storage wrapper for `userver` (cluster + connection pool + query execution).

Under active development!

## Quick start

Create a `storages::odbc::Cluster` with ODBC DSN and execute a query:

```cpp
#include <userver/storages/odbc.hpp>

using namespace std::chrono_literals;

storages::odbc::settings::PoolSettings pool_settings{
.min_size=1,
.max_size=5,
};

storages::odbc::settings::HostSettings host_settings{
.dsn="DRIVER={PostgreSQL Unicode};SERVER=localhost;PORT=15433;DATABASE=postgres;UID=testsuite;PWD=password;",
.pool=pool_settings,
};

storages::odbc::settings::ODBCClusterSettings cluster_settings{
.pools={host_settings},
};

storages::odbc::Cluster cluster{cluster_settings};

auto rs = cluster.Execute(storages::odbc::ClusterHostType::kMaster, "SELECT 1");
auto row = rs[0];
auto field = row[0];
// field.GetInt32() / GetInt64() / GetString() / ...
```

### Reading results

`Execute(...)` returns `storages::odbc::ResultSet`. Each row is `storages::odbc::Row`, and each field is `storages::odbc::Field`:

```cpp
auto rs = cluster.Execute(storages::odbc::ClusterHostType::kMaster,
"SELECT 42, 'test', 1.0, false, null, true");

const auto row = rs[0];
const auto i32 = row[0].GetInt32();
const auto str = row[1].GetString();
if (row[4].IsNull()) {
// ...
}
```

## Deadlines

ODBC operations can be aborted when a deadline is reached.

### Explicit deadline

Use the overloads that accept `engine::Deadline`:

```cpp
#include <userver/engine/deadline.hpp>
#include <userver/storages/odbc.hpp>

using namespace std::chrono_literals;

auto deadline = engine::Deadline::FromDuration(200ms);
auto rs = cluster.Execute(deadline, storages::odbc::ClusterHostType::kMaster, "SELECT 1");
```

Deadlines are also applied to transactions started with `Begin(deadline, ...)`:

```cpp
auto tx = cluster.Begin(deadline, storages::odbc::ClusterHostType::kMaster);
auto rs = tx.Execute("SELECT 1");
tx.Commit(); // deadline is honored internally
```

### Deadline resolution in ODBC

ODBC driver statement timeout (`SQL_ATTR_QUERY_TIMEOUT`) is configured in whole seconds.
As a result, when converting `engine::Deadline` to the ODBC timeout, sub-second deadlines are rounded up to the next full second (so the operation may run slightly longer than the exact deadline).

### Request deadline propagation

If you call ODBC from a request task, the task-inherited request deadline is automatically merged into ODBC deadlines.
If it expires, `storages::odbc::OperationInterrupted` is thrown.

## Transactions

Transactions are created via `Cluster::Begin(...)`.
They execute statements via `Transaction::Execute(...)`, then finish with `Commit()` or `Rollback()`.
If neither commit nor rollback was called, the transaction rolls back on destruction (RAII).

```cpp
auto tx = cluster.Begin(storages::odbc::ClusterHostType::kMaster);

tx.Execute("INSERT INTO t(a) VALUES (1)");
tx.Execute("UPDATE t SET a = a + 1 WHERE a = 1");

tx.Commit();
```

## Exceptions

Common exceptions from `userver::storages::odbc`:

- `storages::odbc::OperationInterrupted` — deadline expired
- `storages::odbc::ConnectionError` — connection / driver failures
- `storages::odbc::StatementError` — statement-level execution errors

## Component Configuration

The ODBC component can be configured in the static config file. Below is the full schema:

### Single Pool Configuration

```yaml
components_manager:
components:
odbc:
dsn: "DRIVER={PostgreSQL Unicode};SERVER=localhost;PORT=5432;DATABASE=mydb;UID=user;PWD=password"
min_pool_size: 1 # optional, default: 1
max_pool_size: 10 # optional, default: 10
dns_resolver: async # optional, default: async (options: async, getaddrinfo)
```

### Multi-Pool Configuration

For master-replica setups or multiple database hosts:

```yaml
components_manager:
components:
odbc:
dns_resolver: async
pools:
- dsn: "DRIVER={PostgreSQL Unicode};SERVER=master.db.local;PORT=5432;DATABASE=mydb;UID=user;PWD=password"
min_pool_size: 2
max_pool_size: 15
- dsn: "DRIVER={PostgreSQL Unicode};SERVER=replica.db.local;PORT=5432;DATABASE=mydb;UID=user;PWD=password"
min_pool_size: 1
max_pool_size: 10
```

### Configuration Options

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `secdist_alias` | string | — | Name of the database in secdist config (for secure credential storage) |
| `dsn` | string | — | ODBC connection string (for single-pool mode) |
| `min_pool_size` | integer | 1 | Minimum number of connections kept in the pool |
| `max_pool_size` | integer | 10 | Maximum number of connections in the pool |
| `dns_resolver` | string | `async` | DNS resolution mode: `async` (non-blocking) or `getaddrinfo` (blocking) |
| `pools` | array | — | List of pool configurations (for multi-pool mode) |

### Secdist Integration

For secure credential storage, you can use secdist instead of putting DSN strings directly in the static config:

```yaml
components_manager:
components:
odbc:
secdist_alias: my_database
min_pool_size: 1
max_pool_size: 10
dns_resolver: async
```

The secdist JSON file should contain:

```json
{
"odbc_settings": {
"databases": {
"my_database": {
"dsn": "DRIVER={PostgreSQL Unicode};SERVER=localhost;PORT=5432;DATABASE=mydb;UID=user;PWD=secret"
}
}
}
}
```

For multiple hosts (master-replica setup):

```json
{
"odbc_settings": {
"databases": {
"my_database": {
"hosts": [
"DRIVER={PostgreSQL Unicode};SERVER=master.db.local;PORT=5432;DATABASE=mydb;UID=user;PWD=secret",
"DRIVER={PostgreSQL Unicode};SERVER=replica.db.local;PORT=5432;DATABASE=mydb;UID=user;PWD=secret"
]
}
}
}
}
```

### DNS Resolution

The `dns_resolver` option controls how hostnames in DSN strings are resolved:

- **`async`** (default): Uses userver's asynchronous DNS resolver. Hostnames are resolved at component startup and replaced with IP addresses in the DSN. This is non-blocking and recommended for production.

- **`getaddrinfo`**: Uses the system's blocking `getaddrinfo()` call. The DSN is passed to the ODBC driver as-is, and hostname resolution happens during connection establishment.

When using `async` mode, the SERVER/HOST parameter in your DSN will be automatically resolved to an IP address before connecting. This allows for proper integration with service discovery and DNS-based load balancing.


### Programmatic Access

You can access the current dynamic config values programmatically:

```cpp
#include <userver/storages/odbc/cluster.hpp>

// Get current default timeouts from dynamic config
auto network_timeout = cluster->GetDefaultNetworkTimeout();
auto statement_timeout = cluster->GetDefaultStatementTimeout();

if (network_timeout.has_value()) {
// Use the configured timeout
}
```
# userver ODBC driver

The driver documentation is maintained in
[`scripts/docs/en/userver/odbc.md`](../scripts/docs/en/userver/odbc.md).
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ description: |
The latter configuration is applied for every non-matching
ODBC component of the service.

Note: Pool size changes require component restart to take effect,
as the underlying connection pool does not support dynamic resizing.
Updates atomically replace pools for new operations. Queries and
transactions already in progress keep their old pools alive until they
finish.
schema:
type: object
example: |
Expand Down
12 changes: 8 additions & 4 deletions odbc/dynamic_configs/USERVER_ODBC_DEFAULT_COMMAND_CONTROL.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
default: {}
description: |
Dynamic config that controls default network and statement timeouts for ODBC driver.
Overrides the built-in timeouts, but could be overridden by explicit
engine::Deadline passed to Execute/Begin methods.
Overrides the built-in timeouts. Individual operations can override these
values with storages::odbc::OptionalCommandControl. A task-inherited
request deadline always caps the resulting deadline.
schema:
type: object
additionalProperties: false
Expand All @@ -12,8 +13,11 @@ schema:
minimum: 1
x-usrv-cpp-type: std::chrono::milliseconds
description: |
Network timeout in milliseconds. Controls how long to wait for
network operations (connection establishment, data transfer).
Overall operation budget in milliseconds. It starts before
connection acquisition, configures the ODBC login timeout, and
caps statement and transaction deadlines. Blocking ODBC calls
are ultimately subject to timeout support and whole-second
resolution of the selected ODBC driver.
statement_timeout_ms:
type: integer
minimum: 1
Expand Down
27 changes: 27 additions & 0 deletions odbc/dynamic_configs/USERVER_ODBC_HANDLERS_COMMAND_CONTROL.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
default: {}
description: |
Dynamic config for per-HTTP-handler ODBC timeouts. Keys are handler paths,
then HTTP methods. Values override only their specified fields from
USERVER_ODBC_DEFAULT_COMMAND_CONTROL and are overridden by named-query and
explicit per-call command controls.
schema:
type: object
additionalProperties:
$ref: "#/definitions/CommandControlByMethodMap"
definitions:
CommandControlByMethodMap:
type: object
additionalProperties:
$ref: "#/definitions/CommandControl"
CommandControl:
type: object
additionalProperties: false
properties:
network_timeout_ms:
type: integer
minimum: 1
x-usrv-cpp-type: std::chrono::milliseconds
statement_timeout_ms:
type: integer
minimum: 1
x-usrv-cpp-type: std::chrono::milliseconds
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
default: {}
description: |
Dynamic config that controls the per-connection prepared statement cache for
ODBC components.

Dictionary keys can be either an ODBC component name or `__default__`.
An exact component entry takes precedence over `__default__`, which takes
precedence over the component's static `max_prepared_cache_size` value.
Zero disables the cache and clears retained prepared statements before the
next operation on each physical connection.
schema:
type: object
example: |
{
"odbc-orders": {
"max_prepared_cache_size": 50
},
"__default__": {
"max_prepared_cache_size": 10
}
}
properties:
__default__:
$ref: "#/definitions/PreparedStatementCacheSettings"
additionalProperties:
$ref: "#/definitions/PreparedStatementCacheSettings"
definitions:
PreparedStatementCacheSettings:
type: object
additionalProperties: false
properties:
max_prepared_cache_size:
type: integer
minimum: 0
default: 0
x-usrv-cpp-type: std::size_t
description: |
Maximum number of prepared parameterized SQL statements
retained per physical ODBC connection. Zero disables and
clears the cache.
required:
- max_prepared_cache_size
Loading
Loading