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
80 changes: 80 additions & 0 deletions docs/postgresql-command-timeout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# PostgreSQL Command Timeout Configuration

This document describes how to configure command timeout for PostgreSQL data sources in Data API Builder.

## Overview

Data API Builder now supports configuring PostgreSQL command timeout through the `command-timeout` option in the data source configuration. This feature allows you to override the default command timeout for all PostgreSQL queries executed by Data API Builder.

## Configuration

Add the `command-timeout` option to your PostgreSQL data source configuration:

```json
{
"data-source": {
"database-type": "postgresql",
"connection-string": "Host=localhost;Database=mydb;Username=user;Password=pass;",
"options": {
"command-timeout": 60
}
}
}
```

### Parameters

- **command-timeout**: Integer value representing the timeout in seconds
- **Type**: `integer`
- **Minimum**: `0`
- **Default**: `30` (if not specified)
- **Description**: Sets the wait time (in seconds) before terminating the attempt to execute a command and generating an error

### Behavior

1. **Override**: The `command-timeout` value from the configuration will override any `CommandTimeout` parameter present in the connection string
2. **Precedence**: Configuration file setting takes priority over connection string setting
3. **Scope**: Applies to all PostgreSQL queries executed through Data API Builder

### Example

```json
{
"$schema": "schemas/dab.draft.schema.json",
"data-source": {
"database-type": "postgresql",
"connection-string": "Host=localhost;Database=bookstore;Username=postgres;Password=password;",
"options": {
"command-timeout": 120
}
},
"runtime": {
"rest": { "enabled": true, "path": "/api" },
"graphql": { "enabled": true, "path": "/graphql" }
},
"entities": {
"Book": {
"source": { "object": "books", "type": "table" },
"permissions": [
{ "role": "anonymous", "actions": [{ "action": "*" }] }
]
}
}
}
```

In this example, all PostgreSQL queries will have a 120-second timeout, regardless of any `CommandTimeout` value in the connection string.

## Implementation Details

The feature is implemented by:

1. **Schema Validation**: The JSON schema validates the `command-timeout` parameter
2. **Options Parsing**: The `PostgreSqlOptions` class parses the timeout value from various data types (integer, string, JsonElement)
3. **Connection String Processing**: The timeout is applied to the Npgsql connection string builder during connection string normalization
4. **Override Logic**: Configuration values take precedence over existing connection string parameters

## Related

- See `samples/postgresql-command-timeout-example.json` for a complete working example
- For other database types, command timeout can be configured directly in the connection string
56 changes: 56 additions & 0 deletions samples/connection-string-timeout-example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{
"$schema": "../schemas/dab.draft.schema.json",
"data-source": {
"database-type": "postgresql",
"connection-string": "Host=localhost;Port=5432;Username=postgres;Password=password;Database=bookshelf;CommandTimeout=120"
},
"runtime": {
"rest": {
"enabled": true,
"path": "/api"
},
"graphql": {
"enabled": true,
"path": "/graphql",
"allow-introspection": true
},
"host": {
"cors": {
"origins": ["*"],
"allow-credentials": false
},
"authentication": {
"provider": "StaticWebApps"
},
"mode": "development"
}
},
"entities": {
"Book": {
"source": {
"object": "books",
"type": "table"
},
"graphql": {
"enabled": true,
"type": {
"singular": "Book",
"plural": "Books"
}
},
"rest": {
"enabled": true
},
"permissions": [
{
"role": "anonymous",
"actions": [
{
"action": "*"
}
]
}
]
}
}
}
59 changes: 59 additions & 0 deletions samples/postgresql-command-timeout-example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{
"$schema": "../schemas/dab.draft.schema.json",
"data-source": {
"database-type": "postgresql",
"connection-string": "Host=localhost;Database=bookstore;Username=postgres;Password=password;",
"options": {
"command-timeout": 60
}
},
"runtime": {
"rest": {
"enabled": true,
"path": "/api"
},
"graphql": {
"enabled": true,
"path": "/graphql",
"allow-introspection": true
},
"host": {
"cors": {
"origins": ["*"],
"allow-credentials": false
},
"authentication": {
"provider": "StaticWebApps"
},
"mode": "development"
}
},
"entities": {
"Book": {
"source": {
"object": "books",
"type": "table"
},
"graphql": {
"enabled": true,
"type": {
"singular": "Book",
"plural": "Books"
}
},
"rest": {
"enabled": true
},
"permissions": [
{
"role": "anonymous",
"actions": [
{
"action": "*"
}
]
}
]
}
}
}
15 changes: 14 additions & 1 deletion schemas/dab.draft.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,12 @@
"set-session-context": {
"type": "boolean",
"description": "Enable sending data to MsSql using session context"
},
"command-timeout": {
"type": "integer",
"description": "Command timeout in seconds for database operations",
"minimum": 0,
"default": 30
}
}
}
Expand All @@ -135,7 +141,14 @@
"options": {
"type": "object",
"additionalProperties": false,
"properties": {},
"properties": {
"command-timeout": {
"type": "integer",
"description": "Command timeout in seconds for database operations",
"minimum": 0,
"default": 30
}
},
Comment on lines +144 to +151
Copy link

Copilot AI Oct 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PostgreSQL schema section adds command-timeout support, but MySQL and other database types are not updated. Consider adding command-timeout support to all database types or documenting why it's only available for specific databases.

Copilot uses AI. Check for mistakes.
"required": []
}
}
Expand Down
58 changes: 58 additions & 0 deletions src/Config/ObjectModel/DataSource.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Text.Json;
using System.Text.Json.Serialization;
using Azure.DataApiBuilder.Config.HealthCheck;
using Azure.DataApiBuilder.Config.NamingPolicies;
Expand Down Expand Up @@ -69,6 +70,12 @@ public int DatasourceThresholdMs
SetSessionContext: ReadBoolOption(namingPolicy.ConvertName(nameof(MsSqlOptions.SetSessionContext))));
}

if (typeof(TOptionType).IsAssignableFrom(typeof(PostgreSqlOptions)))
{
return (TOptionType)(object)new PostgreSqlOptions(
CommandTimeout: ReadIntOption(namingPolicy.ConvertName("command-timeout")));
}

throw new NotSupportedException($"The type {typeof(TOptionType).FullName} is not a supported strongly typed options object");
}

Expand All @@ -92,6 +99,52 @@ private bool ReadBoolOption(string option)
return false;
}

private int? ReadIntOption(string option)
{
if (Options is not null && Options.TryGetValue(option, out object? value))
{
if (value is int intValue)
{
return intValue;
}
else if (value is string stringValue && int.TryParse(stringValue, out int parsedValue))
{
return parsedValue;
}
else if (value is JsonElement jsonElement && jsonElement.ValueKind == JsonValueKind.Number && jsonElement.TryGetInt32(out int jsonValue))
{
return jsonValue;
}
}

return null;
}
Comment on lines +102 to +121
Copy link

Copilot AI Oct 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ReadIntOption method duplicates parsing logic found in GetCommandTimeout. Consider consolidating this logic to avoid code duplication and ensure consistent parsing behavior.

Copilot uses AI. Check for mistakes.

/// <summary>
/// Gets the command timeout value from the options.
/// </summary>
/// <returns>The command timeout in seconds, or 30 (default) if not specified.</returns>
public int GetCommandTimeout()
{
if (Options is not null && Options.TryGetValue("command-timeout", out object? value))
{
if (value is int intValue)
{
return intValue;
}
else if (value is long longValue && longValue <= int.MaxValue && longValue >= int.MinValue)
{
return (int)longValue;
}
else if (value is string stringValue && int.TryParse(stringValue, out int parsedValue))
{
return parsedValue;
}
}

return 30; // default command timeout
}
Comment on lines +127 to +146
Copy link

Copilot AI Oct 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The magic number 30 for default command timeout should be extracted as a named constant to improve maintainability and avoid duplication with the schema default value.

Copilot uses AI. Check for mistakes.

[JsonIgnore]
public string DatabaseTypeNotSupportedMessage => $"The provided database-type value: {DatabaseType} is currently not supported. Please check the configuration file.";
}
Expand All @@ -111,3 +164,8 @@ public record CosmosDbNoSQLDataSourceOptions(string? Database, string? Container
/// Options for MsSql database.
/// </summary>
public record MsSqlOptions(bool SetSessionContext = true) : IDataSourceOptions;

/// <summary>
/// Options for PostgreSQL database.
/// </summary>
public record PostgreSqlOptions(int? CommandTimeout = null) : IDataSourceOptions;
17 changes: 14 additions & 3 deletions src/Config/RuntimeConfigLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ public static bool TryParseConfig(string json,
}
else if (ds.DatabaseType is DatabaseType.PostgreSQL && replaceEnvVar)
{
updatedConnection = GetPgSqlConnectionStringWithApplicationName(connectionValue);
updatedConnection = GetPgSqlConnectionStringWithApplicationName(connectionValue, ds);
}

ds = ds with { ConnectionString = updatedConnection };
Expand Down Expand Up @@ -331,8 +331,9 @@ internal static string GetConnectionStringWithApplicationName(string connectionS
/// else add the Application Name property with DataApiBuilder Application Name based on hosted/oss platform.
/// </summary>
/// <param name="connectionString">Connection string for connecting to database.</param>
/// <returns>Updated connection string with `Application Name` property.</returns>
internal static string GetPgSqlConnectionStringWithApplicationName(string connectionString)
/// <param name="dataSource">The data source configuration, used to get command timeout override.</param>
/// <returns>Updated connection string with `Application Name` property and command timeout.</returns>
internal static string GetPgSqlConnectionStringWithApplicationName(string connectionString, DataSource? dataSource = null)
{
// If the connection string is null, empty, or whitespace, return it as is.
if (string.IsNullOrWhiteSpace(connectionString))
Expand Down Expand Up @@ -369,6 +370,16 @@ internal static string GetPgSqlConnectionStringWithApplicationName(string connec
connectionStringBuilder.ApplicationName += $",{applicationName}";
}

// Apply command timeout from data source configuration if specified (overrides connection string value)
if (dataSource?.Options is not null)
{
PostgreSqlOptions? pgOptions = dataSource.GetTypedOptions<PostgreSqlOptions>();
if (pgOptions?.CommandTimeout is not null)
{
connectionStringBuilder.CommandTimeout = pgOptions.CommandTimeout.Value;
}
}

// Return the updated connection string.
return connectionStringBuilder.ConnectionString;
}
Expand Down
40 changes: 40 additions & 0 deletions src/Service.Tests/Configuration/ConfigurationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5462,6 +5462,46 @@ public static EntityPermission GetMinimalPermissionConfig(string roleName)
);
}

/// <summary>
/// Test that command timeout is properly read from data source options and applied to connection string.
/// </summary>
[TestMethod]
public void TestCommandTimeoutFromDataSourceOptions()
{
// Test SQL Server
Dictionary<string, object> mssqlOptions = new()
{
{ "command-timeout", 60 },
{ "set-session-context", true }
};
DataSource mssqlDataSource = new(DatabaseType.MSSQL, "Server=localhost;Database=test;", mssqlOptions);
Assert.AreEqual(60, mssqlDataSource.GetCommandTimeout());

// Test PostgreSQL
Dictionary<string, object> pgOptions = new()
{
{ "command-timeout", 120 }
};
DataSource pgDataSource = new(DatabaseType.PostgreSQL, "Host=localhost;Database=test;", pgOptions);
Assert.AreEqual(120, pgDataSource.GetCommandTimeout());

// Test MySQL
Dictionary<string, object> mysqlOptions = new()
{
{ "command-timeout", 45 }
};
DataSource mysqlDataSource = new(DatabaseType.MySQL, "Server=localhost;Database=test;", mysqlOptions);
Assert.AreEqual(45, mysqlDataSource.GetCommandTimeout());

// Test default value when not specified
DataSource defaultDataSource = new(DatabaseType.MSSQL, "Server=localhost;Database=test;", new());
Assert.AreEqual(30, defaultDataSource.GetCommandTimeout());

// Test null options
DataSource nullOptionsDataSource = new(DatabaseType.MSSQL, "Server=localhost;Database=test;", null);
Assert.AreEqual(30, nullOptionsDataSource.GetCommandTimeout());
}

/// <summary>
/// Reads configuration file for defined environment to acquire the connection string.
/// CI/CD Pipelines and local environments may not have connection string set as environment variable.
Expand Down
Loading