Skip to main content
Skip to main content

ClickHouse C# client

The official C# client for connecting to ClickHouse. The client source code is available in the GitHub repository. Originally developed by Oleg V. Kozlyuk.

The library provides two main APIs:

  • ClickHouseClient (recommended): A high-level, thread-safe client designed for singleton use. Provides a simple async API for queries and bulk inserts. Best for most applications.

  • ADO.NET (ClickHouseDataSource, ClickHouseConnection, ClickHouseCommand): Standard .NET database abstractions. Required for ORM integration (Dapper, Linq2db) and when you need ADO.NET compatibility. ClickHouseBulkCopy is a helper class for efficiently inserting data using an ADO.NET connection. ClickHouseBulkCopy is deprecated and will be removed in a future release; use ClickHouseClient.InsertBinaryAsync instead.

Both APIs share the same underlying HTTP connection pool and can be used together in the same application.

Migration guide

  1. Update your .csproj file with the new package name ClickHouse.Driver and the latest version on NuGet.
  2. Update all ClickHouse.Client references to ClickHouse.Driver in your codebase.

Supported .NET versions

ClickHouse.Driver supports the following .NET versions:

  • .NET Framework 4.6.2
  • .NET Framework 4.8
  • .NET Standard 2.1
  • .NET 6.0
  • .NET 8.0
  • .NET 9.0
  • .NET 10.0

Installation

Install the package from NuGet:

dotnet add package ClickHouse.Driver

Or using the NuGet Package Manager:

Install-Package ClickHouse.Driver

Quick start

using ClickHouse.Driver;

// Create a client (typically as a singleton)
using var client = new ClickHouseClient("Host=my.clickhouse;Protocol=https;Port=8443;Username=user");

// Execute a query
var version = await client.ExecuteScalarAsync("SELECT version()");
Console.WriteLine(version);

Configuration

There are two ways of configuring your connection to ClickHouse:

  • Connection string: Semicolon-separated key/value pairs that specify the host, authentication credentials, and other connection options.
  • ClickHouseClientSettings object: A strongly typed configuration object that can be loaded from configuration files or set in code.

Below is a full list of all the settings, their default values, and their effects.

Connection settings

PropertyTypeDefaultConnection String KeyDescription
Hoststring"localhost"HostHostname or IP address of the ClickHouse server
Portushort8123 (HTTP) / 8443 (HTTPS)PortPort number; defaults based on protocol
Usernamestring"default"UsernameAuthentication username
Passwordstring""PasswordAuthentication password
Databasestring""DatabaseDefault database; empty uses server/user default
Protocolstring"http"ProtocolConnection protocol: "http" or "https"
PathstringnullPathURL path for reverse proxy scenarios (e.g., /clickhouse)
TimeoutTimeSpan2 minutesTimeoutOperation timeout (stored as seconds in connection string)

Data format & serialization

PropertyTypeDefaultConnection String KeyDescription
UseCompressionbooltrueCompressionEnable gzip compression for data transfer
UseCustomDecimalsbooltrueUseCustomDecimalsUse ClickHouseDecimal for arbitrary precision; if false, uses .NET decimal (128-bit limit)
ReadStringsAsByteArraysboolfalseReadStringsAsByteArraysRead String and FixedString columns as byte[] instead of string; useful for binary data
UseFormDataParametersboolfalseUseFormDataParametersSend parameters as form data instead of URL query string
JsonReadModeJsonReadModeBinaryJsonReadModeHow JSON data is returned: Binary (returns JsonObject) or String (returns raw JSON string)
JsonWriteModeJsonWriteModeStringJsonWriteModeHow JSON data is sent: String (serializes via JsonSerializer, accepts all inputs) or Binary (registered POCOs only with type hints)

Session management

PropertyTypeDefaultConnection String KeyDescription
UseSessionboolfalseUseSessionEnable stateful sessions; serializes requests
SessionIdstringnullSessionIdSession ID; auto-generates GUID if null and UseSession is true
Note

The UseSession flag enables persistence of the server session, allowing use of SET statements and temporary tables. Sessions will be reset after 60 seconds of inactivity (default timeout). Session lifetime can be extended by setting session settings via ClickHouse statements or the server configuration.

The ClickHouseConnection class normally allows for parallel operation (multiple threads can run queries concurrently). However, enabling UseSession flag will limit that to one active query per connection at any moment of time (this is a server-side limitation).

Security

PropertyTypeDefaultConnection String KeyDescription
SkipServerCertificateValidationboolfalseSkip HTTPS certificate validation; not for production use

HTTP client configuration

PropertyTypeDefaultConnection String KeyDescription
HttpClientHttpClientnullCustom pre-configured HttpClient instance
HttpClientFactoryIHttpClientFactorynullCustom factory for creating HttpClient instances
HttpClientNamestringnullName for HttpClientFactory to create specific client

Logging & debugging

PropertyTypeDefaultConnection String KeyDescription
LoggerFactoryILoggerFactorynullLogger factory for diagnostic logging
EnableDebugModeboolfalseEnable .NET network tracing (requires LoggerFactory with level set to Trace); significant performance impact

Custom settings & roles

PropertyTypeDefaultConnection String KeyDescription
CustomSettingsIDictionary<string, object>Emptyset_* prefixClickHouse server settings, see note below
RolesIReadOnlyList<string>EmptyRolesComma-separated ClickHouse roles (e.g., Roles=admin,reader)
Note

When using a connection string to set custom settings, use the set_ prefix, e.g. "set_max_threads=4". When using a ClickHouseClientSettings object, do not use the set_ prefix.

For a full list of available settings, see here.


Connection string examples

Basic connection

Host=localhost;Port=8123;Username=default;Password=secret;Database=mydb

With custom ClickHouse settings

Host=localhost;set_max_threads=4;set_readonly=1;set_max_memory_usage=10000000000

QueryOptions

QueryOptions allows you to override client-level settings on a per-query basis. All properties are optional and only override the client defaults when specified.

PropertyTypeDescription
QueryIdstringCustom query identifier for tracking in system.query_log or cancellation
DatabasestringOverride the default database for this query
RolesIReadOnlyList<string>Override client roles for this query
CustomSettingsIDictionary<string, object>ClickHouse server settings for this query (e.g., max_threads)
CustomHeadersIDictionary<string, string>Additional HTTP headers for this query
UseSessionbool?Override session behavior for this query
SessionIdstringSession ID for this query (requires UseSession = true)
BearerTokenstringOverride authentication token for this query
MaxExecutionTimeTimeSpan?Server-side query timeout (passed as max_execution_time setting); server cancels query if exceeded

Example:

var options = new QueryOptions
{
    QueryId = "report-2024-001",
    Database = "analytics",
    CustomSettings = new Dictionary<string, object>
    {
        { "max_threads", 4 },
        { "max_memory_usage", 10_000_000_000 }
    },
    MaxExecutionTime = TimeSpan.FromMinutes(5)
};

var reader = await client.ExecuteReaderAsync(
    "SELECT * FROM large_table",
    parameters: null,
    options: options
);

InsertOptions

InsertOptions extends QueryOptions with settings specific to bulk insert operations via InsertBinaryAsync.

PropertyTypeDefaultDescription
BatchSizeint100,000Number of rows per batch
MaxDegreeOfParallelismint1Number of parallel batch uploads
FormatRowBinaryFormatRowBinaryBinary format: RowBinary or RowBinaryWithDefaults

All QueryOptions properties are also available on InsertOptions.

Example:

var insertOptions = new InsertOptions
{
    BatchSize = 50_000,
    MaxDegreeOfParallelism = 4,
    QueryId = "bulk-import-001"
};

long rowsInserted = await client.InsertBinaryAsync(
    "my_table",
    columns,
    rows,
    insertOptions
);

ClickHouseClient

ClickHouseClient is the recommended API for interacting with ClickHouse. It is thread-safe, designed for singleton use, and manages HTTP connection pooling internally.

Creating a client

Create a ClickHouseClient with a connection string or a ClickHouseClientSettings object. See the Configuration section for available options.

The details for your ClickHouse Cloud service are available in the ClickHouse Cloud console.

Select a service and click Connect:

Choose C#. Connection details are displayed below.

If you are using self-managed ClickHouse, the connection details are set by your ClickHouse administrator.

Using a connection string:

using ClickHouse.Driver;

using var client = new ClickHouseClient("Host=localhost;Username=default;Password=secret");

Or using ClickHouseClientSettings:

using ClickHouse.Driver;

var settings = new ClickHouseClientSettings
{
    Host = "localhost",
    Username = "default",
    Password = "secret"
};
using var client = new ClickHouseClient(settings);

For dependency injection scenarios, use IHttpClientFactory:

// In your DI configuration
services.AddHttpClient("ClickHouse", client =>
{
    client.Timeout = TimeSpan.FromMinutes(5);
}).ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
{
    AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
});

// Create client with factory
var factory = serviceProvider.GetRequiredService<IHttpClientFactory>();
var client = new ClickHouseClient("Host=localhost", factory, "ClickHouse");
Note

ClickHouseClient is designed to be long-lived and shared across your application. Create it once (typically as a singleton) and reuse it for all database operations. The client manages HTTP connection pooling internally.


Executing queries

Use ExecuteNonQueryAsync for statements that don't return results:

// Create a table
await client.ExecuteNonQueryAsync(
    "CREATE TABLE IF NOT EXISTS default.my_table (id Int64, name String) ENGINE = Memory"
);

// Drop a table
await client.ExecuteNonQueryAsync("DROP TABLE IF EXISTS default.my_table");

Use ExecuteScalarAsync to retrieve a single value:

var count = await client.ExecuteScalarAsync("SELECT count() FROM default.my_table");
Console.WriteLine($"Row count: {count}");

var version = await client.ExecuteScalarAsync("SELECT version()");
Console.WriteLine($"Server version: {version}");

Inserting data

Parameterized inserts

Insert data using parameterized queries with ExecuteNonQueryAsync. Parameter types must be specified in the SQL using {name:Type} syntax:

using ClickHouse.Driver;
using ClickHouse.Driver.ADO.Parameters;

var parameters = new ClickHouseParameterCollection();
parameters.Add("id", 1L);
parameters.Add("name", "Alice");

await client.ExecuteNonQueryAsync(
    "INSERT INTO default.my_table (id, name) VALUES ({id:Int64}, {name:String})",
    parameters
);

Bulk inserts

Use InsertBinaryAsync for inserting large numbers of rows efficiently. It streams data using ClickHouse's native row binary format, supports parallel batch uploads, and avoids "URL too long" errors that can occur with parameterized queries.

// Prepare data as IEnumerable<object[]>
var rows = Enumerable.Range(0, 1_000_000)
    .Select(i => new object[] { (long)i, $"value{i}" });

var columns = new[] { "id", "name" };

// Basic insert
long rowsInserted = await client.InsertBinaryAsync("default.my_table", columns, rows);
Console.WriteLine($"Rows inserted: {rowsInserted}");

For large datasets, configure batching and parallelism with InsertOptions:

var options = new InsertOptions
{
    BatchSize = 100_000,           // Rows per batch (default: 100,000)
    MaxDegreeOfParallelism = 4     // Parallel batch uploads (default: 1)
};
Note
  • The client automatically fetches table structure via SELECT * FROM <table> WHERE 1=0 before inserting. Provided values must match the target column types.
  • When MaxDegreeOfParallelism > 1, batches are uploaded in parallel. Sessions are not compatible with parallel insertion; either disable sessions or set MaxDegreeOfParallelism = 1.
  • Use RowBinaryFormat.RowBinaryWithDefaults in InsertOptions.Format if you want the server to apply DEFAULT values for columns not provided.

Reading data

Use ExecuteReaderAsync to execute SELECT queries. The returned ClickHouseDataReader provides typed access to result columns via methods like GetInt64(), GetString(), and GetFieldValue<T>().

Call Read() to advance to the next row. It returns false when there are no more rows. Access columns by index (0-based) or by column name.

using ClickHouse.Driver.ADO.Parameters;

var parameters = new ClickHouseParameterCollection();
parameters.Add("max_id", 100L);

var reader = await client.ExecuteReaderAsync(
    "SELECT * FROM default.my_table WHERE id < {max_id:Int64}",
    parameters
);

while (reader.Read())
{
    Console.WriteLine($"Id: {reader.GetInt64(0)}, Name: {reader.GetString(1)}");
}

SQL parameters

In ClickHouse, the standard format for query parameters in SQL queries is {parameter_name:DataType}.

Examples:

SELECT {value:Array(UInt16)} as a
SELECT * FROM table WHERE val = {tuple_in_tuple:Tuple(UInt8, Tuple(String, UInt8))}
INSERT INTO table VALUES ({val1:Int32}, {val2:Array(UInt8)})
Note

SQL 'bind' parameters are passed as HTTP URI query parameters, so using too many of them may result in a "URL too long" exception. Use InsertBinaryAsync for bulk data insertion to avoid this limitation.


Query ID

Every query is assigned a unique query_id that can be used to fetch data from the system.query_log table or cancel long-running queries. You can specify a custom query ID via QueryOptions:

var options = new QueryOptions
{
    QueryId = $"report-{Guid.NewGuid()}"
};

var reader = await client.ExecuteReaderAsync(
    "SELECT * FROM large_table",
    parameters: null,
    options: options
);
Tip

If you are specifying a custom QueryId, ensure it is unique for every call. A random GUID is a good choice.


Raw streaming

Use ExecuteRawResultAsync to stream query results in a specific format directly, bypassing the data reader. This is useful for exporting data to files or passing through to other systems:

using var result = await client.ExecuteRawResultAsync(
    "SELECT * FROM default.my_table LIMIT 100 FORMAT JSONEachRow"
);

await using var stream = await result.ReadAsStreamAsync();
using var reader = new StreamReader(stream);
var json = await reader.ReadToEndAsync();

Common formats: JSONEachRow, CSV, TSV, Parquet, Native. See the formats documentation for all options.


Raw stream insert

Use InsertRawStreamAsync to insert data directly from file or memory streams in formats like CSV, JSON, Parquet, or any supported ClickHouse format.

Insert from a CSV file:

await using var fileStream = File.OpenRead("data.csv");

using var response = await client.InsertRawStreamAsync(
    table: "my_table",
    stream: fileStream,
    format: "CSV",
    columns: ["id", "product", "price"] // Optional: specify columns
);
Note

See the format settings documentation for options to control data ingestion behavior.


More examples

For additional practical usage examples, see the examples directory in the GitHub repository.

ADO.NET

The library provides full ADO.NET support through ClickHouseConnection, ClickHouseCommand, and ClickHouseDataReader. This API is required for ORM integration (Dapper, Linq2db) and when you need standard .NET database abstractions.

Lifetime management with ClickHouseDataSource

Always create connections from a ClickHouseDataSource to ensure proper lifetime management and connection pooling. The DataSource manages a single ClickHouseClient internally, and all connections share its HTTP connection pool.

using ClickHouse.Driver.ADO;

// Create DataSource once (register as singleton in DI)
var dataSource = new ClickHouseDataSource("Host=localhost;Username=default;Password=secret");

// Create lightweight connections as needed
await using var connection = await dataSource.OpenConnectionAsync();

// Use the connection
await using var command = connection.CreateCommand("SELECT version()");
var version = await command.ExecuteScalarAsync();

For dependency injection:

// In Startup.cs or Program.cs
services.AddSingleton(sp =>
{
    var factory = sp.GetRequiredService<IHttpClientFactory>();
    return new ClickHouseDataSource("Host=localhost", factory, "ClickHouse");
});

// In your service
public class MyService
{
    private readonly ClickHouseDataSource _dataSource;

    public MyService(ClickHouseDataSource dataSource)
    {
        _dataSource = dataSource;
    }

    public async Task DoWorkAsync()
    {
        await using var connection = await _dataSource.OpenConnectionAsync();
        // Use connection...
    }
}
Note

Do not create ClickHouseConnection directly in production code. Each direct instantiation creates a new HTTP client and connection pool, which can lead to socket exhaustion under load:

// DON'T DO THIS - creates new connection pool each time
using var conn = new ClickHouseConnection("Host=localhost");
await conn.OpenAsync();

Instead, always use ClickHouseDataSource or share a single ClickHouseClient instance.


Using ClickHouseCommand

Create commands from a connection to execute SQL:

await using var connection = await dataSource.OpenConnectionAsync();

// Create command with SQL
await using var command = connection.CreateCommand("SELECT * FROM my_table WHERE id = {id:Int64}");
command.AddParameter("id", 42L);

// Execute and read results
await using var reader = await command.ExecuteReaderAsync();
while (reader.Read())
{
    Console.WriteLine($"Name: {reader.GetString("name")}");
}

Command methods:

  • ExecuteNonQueryAsync() - For INSERT, UPDATE, DELETE, DDL statements
  • ExecuteScalarAsync() - Returns first column of first row
  • ExecuteReaderAsync() - Returns a ClickHouseDataReader for iterating results

Using ClickHouseDataReader

The ClickHouseDataReader provides typed access to query results:

await using var reader = await command.ExecuteReaderAsync();

while (reader.Read())
{
    // Access by column index
    var id = reader.GetInt64(0);
    var name = reader.GetString(1);

    // Access by column name
    var email = reader.GetString("email");

    // Generic access
    var timestamp = reader.GetFieldValue<DateTime>("created_at");

    // Check for null
    if (!reader.IsDBNull("optional_field"))
    {
        var value = reader.GetString("optional_field");
    }
}

Best practices

Connection lifetime and pooling

ClickHouse.Driver uses System.Net.Http.HttpClient under the hood. HttpClient has a per-endpoint connection pool. As a consequence:

  • Database sessions are multiplexed through HTTP connections managed by the connection pool.
  • HTTP connections are recycled automatically by the pool.
  • Connections can stay alive after ClickHouseClient or ClickHouseConnection objects are disposed.

Recommended patterns:

ScenarioRecommended Approach
General useUse a singleton ClickHouseClient
ADO.NET / ORMsUse ClickHouseDataSource (creates connections that share the same pool)
DI environmentsRegister ClickHouseClient or ClickHouseDataSource as singleton with IHttpClientFactory
References

When using a custom HttpClient or HttpClientFactory, ensure that the PooledConnectionIdleTimeout is set to a value smaller than the server's keep_alive_timeout, in order to avoid errors due to half-closed connections. The default keep_alive_timeout for Cloud deployments is 10 seconds.

Note

Avoid creating multiple ClickHouseClient or standalone ClickHouseConnection instances without a shared HttpClient. Each instance creates its own connection pool.


DateTime handling

  1. Use UTC whenever possible. Store timestamps as DateTime('UTC') columns and use DateTimeKind.Utc in your code. This eliminates timezone ambiguity.

  2. Use DateTimeOffset for explicit timezone handling. It always represents a specific instant and includes the offset information.

  3. Specify timezone in SQL type hints. When using parameters with Unspecified DateTime values targeting non-UTC columns, include the timezone in the SQL:

    var parameters = new ClickHouseParameterCollection();
    parameters.Add("dt", myDateTime);
    
    await client.ExecuteNonQueryAsync(
        "INSERT INTO table (dt) VALUES ({dt:DateTime('Europe/Amsterdam')})",
        parameters
    );
    

Async inserts

Async inserts shift batching responsibility from the client to the server. Instead of requiring client-side batching, the server buffers incoming data and flushes it to storage based on configurable thresholds. This is useful for high-concurrency scenarios like observability workloads where many agents send small payloads.

Enable async inserts via CustomSettings or the connection string:

// Using CustomSettings
var settings = new ClickHouseClientSettings("Host=localhost");
settings.CustomSettings["async_insert"] = 1;
settings.CustomSettings["wait_for_async_insert"] = 1; // Recommended: wait for flush acknowledgment

// Or via connection string
// "Host=localhost;set_async_insert=1;set_wait_for_async_insert=1"

Two modes (controlled by wait_for_async_insert):

ModeBehaviorUse case
wait_for_async_insert=1Insert returns after data is flushed to disk. Errors are returned to the client.Recommended for most workloads
wait_for_async_insert=0Insert returns immediately when data is buffered. No guarantee data will be persisted.Only when data loss is acceptable
Note

With wait_for_async_insert=0, errors only surface during flush and cannot be traced back to the original insert. The client also provides no backpressure, risking server overload.

Key settings:

SettingDescription
async_insert_max_data_sizeFlush when buffer reaches this size (bytes)
async_insert_busy_timeout_msFlush after this timeout (milliseconds)
async_insert_max_query_numberFlush after this many queries accumulate

Sessions

Only enable sessions when you need stateful server-side features, e.g.:

  • Temporary tables (CREATE TEMPORARY TABLE)
  • Maintaining query context across multiple statements
  • Session-level settings (SET max_threads = 4)

When sessions are enabled, requests are serialized to prevent concurrent use of the same session. This adds overhead for workloads that don't require session state.

var settings = new ClickHouseClientSettings
{
    Host = "localhost",
    UseSession = true,
    SessionId = "my-session", // Optional -- will be auto-generated if not provided
};

using var client = new ClickHouseClient(settings);

await client.ExecuteNonQueryAsync("CREATE TEMPORARY TABLE temp_ids (id UInt64)");
await client.ExecuteNonQueryAsync("INSERT INTO temp_ids VALUES (1), (2), (3)");

var reader = await client.ExecuteReaderAsync(
    "SELECT * FROM users WHERE id IN (SELECT id FROM temp_ids)"
);

Using ADO.NET (for ORM compatibility):

var settings = new ClickHouseClientSettings
{
    Host = "localhost",
    UseSession = true,
    SessionId = "my-session",
};

var dataSource = new ClickHouseDataSource(settings);
await using var connection = await dataSource.OpenConnectionAsync();

await using var cmd1 = connection.CreateCommand("CREATE TEMPORARY TABLE temp_ids (id UInt64)");
await cmd1.ExecuteNonQueryAsync();

await using var cmd2 = connection.CreateCommand("INSERT INTO temp_ids VALUES (1), (2), (3)");
await cmd2.ExecuteNonQueryAsync();

await using var cmd3 = connection.CreateCommand("SELECT * FROM users WHERE id IN (SELECT id FROM temp_ids)");
await using var reader = await cmd3.ExecuteReaderAsync();

Supported data types

ClickHouse.Driver supports all ClickHouse data types. The tables below show the mappings between ClickHouse types and native .NET types when reading data from the database.

Type mapping: reading from ClickHouse

Integer types

ClickHouse Type.NET Type
Int8sbyte
UInt8byte
Int16short
UInt16ushort
Int32int
UInt32uint
Int64long
UInt64ulong
Int128BigInteger
UInt128BigInteger
Int256BigInteger
UInt256BigInteger

Floating point types

ClickHouse Type.NET Type
Float32float
Float64double
BFloat16float

Decimal types

ClickHouse Type.NET Type
Decimal(P, S)decimal / ClickHouseDecimal
Decimal32(S)decimal / ClickHouseDecimal
Decimal64(S)decimal / ClickHouseDecimal
Decimal128(S)decimal / ClickHouseDecimal
Decimal256(S)decimal / ClickHouseDecimal
Note

Decimal type conversion is controlled via the UseCustomDecimals setting.


Boolean type

ClickHouse Type.NET Type
Boolbool

String types

ClickHouse Type.NET Type
Stringstring
FixedString(N)string
Note

By default, both String and FixedString(N) columns are returned as string. Set ReadStringsAsByteArrays=true in your connection string to read them as byte[] instead. This is useful when storing binary data that may not be valid UTF-8.


Date and time types

ClickHouse Type.NET Type
DateDateTime
Date32DateTime
DateTimeDateTime
DateTime32DateTime
DateTime64DateTime
TimeTimeSpan
Time64TimeSpan

ClickHouse stores DateTime and DateTime64 values internally as Unix timestamps (seconds or sub-second units since epoch). While the storage is always in UTC, columns can have an associated timezone that affects how values are displayed and interpreted.

When reading DateTime values, the DateTime.Kind property is set based on the column's timezone:

Column DefinitionReturned DateTime.KindNotes
DateTime('UTC')UtcExplicit UTC timezone
DateTime('Europe/Amsterdam')UnspecifiedOffset applied
DateTimeUnspecifiedWall-clock time preserved as-is

For non-UTC columns, the returned DateTime represents the wall-clock time in that timezone. Use ClickHouseDataReader.GetDateTimeOffset() to get a DateTimeOffset with the correct offset for that timezone:

var reader = (ClickHouseDataReader)await connection.ExecuteReaderAsync(
    "SELECT toDateTime('2024-06-15 14:30:00', 'Europe/Amsterdam')");
reader.Read();

var dt = reader.GetDateTime(0);    // 2024-06-15 14:30:00, Kind=Unspecified
var dto = reader.GetDateTimeOffset(0); // 2024-06-15 14:30:00 +02:00 (CEST)

For columns without an explicit timezone (i.e., DateTime instead of DateTime('Europe/Amsterdam')), the driver returns a DateTime with Kind=Unspecified. This preserves the wall-clock time exactly as stored without making assumptions about timezone.

If you need timezone-aware behavior for columns without explicit timezones, either:

  1. Use explicit timezones in your column definitions: DateTime('UTC') or DateTime('Europe/Amsterdam')
  2. Apply the timezone yourself after reading.

JSON type

ClickHouse Type.NET TypeNotes
JsonJsonObjectDefault (JsonReadMode=Binary)
JsonstringWhen JsonReadMode=String

The return type for JSON columns is controlled by the JsonReadMode setting:

  • Binary (default): Returns System.Text.Json.Nodes.JsonObject. Provides structured access to JSON data, but specialized ClickHouse types (like IP addresses, UUIDs, large decimals) are converted to their string representations within the JSON structure.

  • String: Returns the raw JSON as a string. Preserves the exact JSON representation from ClickHouse, which is useful when you need to pass the JSON through without parsing, or when you want to handle deserialization yourself.

// Configure string mode via settings
var settings = new ClickHouseClientSettings("Host=localhost")
{
    JsonReadMode = JsonReadMode.String
};

// Or via connection string
// "Host=localhost;JsonReadMode=String"

Other types

ClickHouse Type.NET Type
UUIDGuid
IPv4IPAddress
IPv6IPAddress
NothingDBNull
DynamicSee note
Array(T)T[]
Tuple(T1, T2, ...)Tuple<T1, T2, ...> / LargeTuple
Map(K, V)Dictionary<K, V>
Nullable(T)T?
Enum8string
Enum16string
LowCardinality(T)Same as T
SimpleAggregateFunctionSame as underlying type
Nested(...)Tuple[]
Variant(T1, T2, ...)See note
QBit(T, dimension)T[]
Note

The Dynamic and Variant types will be converted to the corresponding type for the actual underlying type in each row.


Geometry types

ClickHouse Type.NET Type
PointTuple<double, double>
RingTuple<double, double>[]
LineStringTuple<double, double>[]
PolygonRing[]
MultiLineStringLineString[]
MultiPolygonPolygon[]
GeometrySee note
Note

The Geometry type is a Variant type that can hold any of the geometry types. It will be converted to the corresponding type.


Type mapping: writing to ClickHouse

When inserting data, the driver converts .NET types to their corresponding ClickHouse types. The tables below show which .NET types are accepted for each ClickHouse column type.

Integer types

ClickHouse TypeAccepted .NET TypesNotes
Int8sbyte, any Convert.ToSByte() compatible
UInt8byte, any Convert.ToByte() compatible
Int16short, any Convert.ToInt16() compatible
UInt16ushort, any Convert.ToUInt16() compatible
Int32int, any Convert.ToInt32() compatible
UInt32uint, any Convert.ToUInt32() compatible
Int64long, any Convert.ToInt64() compatible
UInt64ulong, any Convert.ToUInt64() compatible
Int128BigInteger, decimal, double, float, int, uint, long, ulong, any Convert.ToInt64() compatible
UInt128BigInteger, decimal, double, float, int, uint, long, ulong, any Convert.ToInt64() compatible
Int256BigInteger, decimal, double, float, int, uint, long, ulong, any Convert.ToInt64() compatible
UInt256BigInteger, decimal, double, float, int, uint, long, ulong, any Convert.ToInt64() compatible

Floating point types

ClickHouse TypeAccepted .NET TypesNotes
Float32float, any Convert.ToSingle() compatible
Float64double, any Convert.ToDouble() compatible
BFloat16float, any Convert.ToSingle() compatibleTruncates to 16-bit brain float format

Boolean type

ClickHouse TypeAccepted .NET TypesNotes
Boolbool

String types

ClickHouse TypeAccepted .NET TypesNotes
Stringstring, byte[], ReadOnlyMemory<byte>, StreamBinary types written directly; streams can be seekable or non-seekable
FixedString(N)string, byte[], ReadOnlyMemory<byte>, StreamString is UTF-8 encoded and padded; binary types must be exactly N bytes

Date and time types

ClickHouse TypeAccepted .NET TypesNotes
DateDateTime, DateTimeOffset, DateOnly, NodaTime typesConverted to Unix days as UInt16
Date32DateTime, DateTimeOffset, DateOnly, NodaTime typesConverted to Unix days as Int32
DateTimeDateTime, DateTimeOffset, DateOnly, NodaTime typesSee below for details
DateTime32DateTime, DateTimeOffset, DateOnly, NodaTime typesSame as DateTime
DateTime64DateTime, DateTimeOffset, DateOnly, NodaTime typesPrecision based on Scale parameter
TimeTimeSpan, intClamped to ±999:59:59; int treated as seconds
Time64TimeSpan, decimal, double, float, int, long, stringString parsed as [-]HHH:MM:SS[.fraction]; clamped to ±999:59:59.999999999

The driver respects DateTime.Kind when writing values:

DateTime.KindHTTP ParametersBulk
UtcInstant preservedInstant preserved
LocalInstant preservedInstant preserved
UnspecifiedTreated as wall-clock in parameter type's timezone (defaults to UTC)Treated as wall-clock in column's timezone

DateTimeOffset values always preserve the exact instant.

Example: UTC DateTime (instant preserved)

var utcTime = new DateTime(2024, 1, 15, 12, 0, 0, DateTimeKind.Utc);
// Stored as 12:00 UTC
// Read from DateTime('Europe/Amsterdam') column: 13:00 (UTC+1)
// Read from DateTime('UTC') column: 12:00 UTC

Example: unspecified DateTime (wall-clock time)

var wallClock = new DateTime(2024, 1, 15, 14, 30, 0, DateTimeKind.Unspecified);
// Written to DateTime('Europe/Amsterdam') column: stored as 14:30 Amsterdam time
// Read back from DateTime('Europe/Amsterdam') column: 14:30

Recommendation: for simplest and most predictable behavior, use DateTimeKind.Utc or DateTimeOffset for all DateTime operations. This ensures your code works consistently regardless of server timezone, client timezone, or column timezone.

HTTP parameters vs bulk copy

There is an important difference between HTTP parameter binding and bulk copy when writing Unspecified DateTime values:

Bulk Copy knows the target column's timezone and correctly interprets Unspecified values in that timezone.

HTTP Parameters do not automatically know the column timezone. You must specify it in the SQL type hint:

// CORRECT: Timezone in SQL type hint - type is extracted automatically
command.CommandText = "INSERT INTO table (dt_amsterdam) VALUES ({dt:DateTime('Europe/Amsterdam')})";
command.AddParameter("dt", myDateTime);

// INCORRECT: Without timezone hint, interpreted as UTC
command.CommandText = "INSERT INTO table (dt_amsterdam) VALUES ({dt:DateTime})";
command.AddParameter("dt", myDateTime);
// String value "2024-01-15 14:30:00" interpreted as UTC, not Amsterdam time!
DateTime.KindTarget ColumnHTTP Param (with tz hint)HTTP Param (no tz hint)Bulk Copy
UtcUTCInstant preservedInstant preservedInstant preserved
UtcEurope/AmsterdamInstant preservedInstant preservedInstant preserved
LocalAnyInstant preservedInstant preservedInstant preserved
UnspecifiedUTCTreated as UTCTreated as UTCTreated as UTC
UnspecifiedEurope/AmsterdamTreated as Amsterdam timeTreated as UTCTreated as Amsterdam time

Decimal types

ClickHouse TypeAccepted .NET TypesNotes
Decimal(P,S)decimal, ClickHouseDecimal, any Convert.ToDecimal() compatibleThrows OverflowException if exceeds precision
Decimal32decimal, ClickHouseDecimal, any Convert.ToDecimal() compatibleMax precision 9
Decimal64decimal, ClickHouseDecimal, any Convert.ToDecimal() compatibleMax precision 18
Decimal128decimal, ClickHouseDecimal, any Convert.ToDecimal() compatibleMax precision 38
Decimal256decimal, ClickHouseDecimal, any Convert.ToDecimal() compatibleMax precision 76

JSON type

ClickHouse TypeAccepted .NET TypesNotes
Jsonstring, JsonObject, JsonNode, any objectBehavior depends on JsonWriteMode setting

The behavior when writing JSON is controlled by the JsonWriteMode setting:

Input TypeJsonWriteMode.String (default)JsonWriteMode.Binary
stringPassed through directlyThrows ArgumentException
JsonObjectSerialized via ToJsonString()Throws ArgumentException
JsonNodeSerialized via ToJsonString()Throws ArgumentException
Registered POCOSerialized via JsonSerializer.Serialize()Binary encoding with type hints, custom path attributes supported
Unregistered POCO / Anonymous objectSerialized via JsonSerializer.Serialize()Throws ClickHouseJsonSerializationException
  • String (default): Accepts string, JsonObject, JsonNode, or any object. All inputs are serialized via System.Text.Json.JsonSerializer and sent as JSON strings for server-side parsing. This is the most flexible mode and works without type registration.

  • Binary: Only accepts registered POCO types. Data is converted to ClickHouse's binary JSON format client-side with full type hint support. Requires calling connection.RegisterJsonSerializationType<T>() before use. Writing string or JsonNode values in this mode throws ArgumentException.

// Default String mode works with any input
await client.InsertBinaryAsync(
    "my_table",
    new[] { "id", "data" },
    new[] { new object[] { 1u, new { name = "test", value = 42 } } }
);

// Binary mode requires explicit opt-in and type registration
var settings = new ClickHouseClientSettings("Host=localhost")
{
    JsonWriteMode = JsonWriteMode.Binary
};
using var client = new ClickHouseClient(settings);
client.RegisterJsonSerializationType<MyPocoType>();
Typed JSON columns

When a JSON column has type hints (e.g., JSON(id UInt64, price Decimal128(2))), the driver uses these hints to serialize values with full type fidelity. This preserves precision for types like UInt64, Decimal, UUID, and DateTime64 that would otherwise lose precision when serialized as generic JSON.

POCO serialization

POCOs can be written to JSON columns in two ways depending on the JsonWriteMode:

String mode (default): POCOs are serialized via System.Text.Json.JsonSerializer. No type registration is required. This is the simplest approach and works with anonymous objects.

Binary mode: POCOs are serialized using the driver's binary JSON format with full type hint support. Types must be registered with connection.RegisterJsonSerializationType<T>() before use. This mode supports custom path mappings via attributes:

  • [ClickHouseJsonPath("path")]: Maps a property to a custom JSON path. Useful for nested structures or when the property name differs from the desired JSON key. Only works in Binary mode.

  • [ClickHouseJsonIgnore]: Excludes a property from serialization. Only works in Binary mode.

CREATE TABLE events (
    id UInt32,
    data JSON(`user.id` Int64, `user.name` String, Timestamp DateTime64(3))
) ENGINE = MergeTree() ORDER BY id
using ClickHouse.Driver.Json;

public class UserEvent
{
    [ClickHouseJsonPath("user.id")]
    public long UserId { get; set; }

    [ClickHouseJsonPath("user.name")]
    public string UserName { get; set; }

    public DateTime Timestamp { get; set; }

    [ClickHouseJsonIgnore]
    public string InternalData { get; set; }  // Not serialized
}

// For Binary mode: Register the type and enable Binary mode
var settings = new ClickHouseClientSettings("Host=localhost") { JsonWriteMode = JsonWriteMode.Binary };
using var client = new ClickHouseClient(settings);
client.RegisterJsonSerializationType<UserEvent>();

// Insert POCO - serialized to JSON with nested structure via custom path attributes
await client.InsertBinaryAsync(
    "events",
    new[] { "id", "data" },
    new[] { new object[] { 1u, new UserEvent { UserId = 123, UserName = "Alice", Timestamp = DateTime.UtcNow } } }
);
// Resulting JSON: {"user": {"id": 123, "name": "Alice"}, "Timestamp": "2024-01-15T..."}

Property name matching with column type hints is case-sensitive. A property UserId will only match a hint defined as UserId, not userid. This matches ClickHouse behavior which allows paths like userName and UserName to coexist as separate fields.

Limitations (Binary mode only):

  • POCO types must be registered on the connection with connection.RegisterJsonSerializationType<T>() before serialization. Attempting to serialize an unregistered type throws ClickHouseJsonSerializationException.
  • Dictionary and array/list properties require type hints in the column definition to be serialized correctly. Without hints, use String mode instead.
  • Null values in POCO properties are only written when the path has a Nullable(T) type hint in the column definition. ClickHouse doesn't allow Nullable types inside dynamic JSON paths, so un-hinted null properties are skipped.
  • ClickHouseJsonPath and ClickHouseJsonIgnore attributes are ignored in String mode (they only work in Binary mode).

Other types

ClickHouse TypeAccepted .NET TypesNotes
UUIDGuid, stringString parsed as Guid
IPv4IPAddress, stringMust be IPv4; string parsed via IPAddress.Parse()
IPv6IPAddress, stringMust be IPv6; string parsed via IPAddress.Parse()
NothingAnyWrites nothing (no-op)
DynamicNot supported (throws NotImplementedException)
Array(T)IList, nullNull writes empty array
Tuple(T1, T2, ...)ITuple, IListElement count must match tuple arity
Map(K, V)IDictionary
Nullable(T)null, DBNull, or types accepted by TWrites null flag byte before value
Enum8string, sbyte, numeric typesString looked up in enum dictionary
Enum16string, short, numeric typesString looked up in enum dictionary
LowCardinality(T)Types accepted by TDelegates to underlying type
SimpleAggregateFunctionTypes accepted by underlying typeDelegates to underlying type
Nested(...)IList of tuplesElement count must match field count
Variant(T1, T2, ...)Value matching one of T1, T2, ...Throws ArgumentException if no type match
QBit(T, dim)IListDelegates to Array; dimension is metadata only

Geometry types

ClickHouse TypeAccepted .NET TypesNotes
PointSystem.Drawing.Point, ITuple, IList (2 elements)
RingIList of Points
LineStringIList of Points
PolygonIList of Rings
MultiLineStringIList of LineStrings
MultiPolygonIList of Polygons
GeometryAny geometry type aboveVariant of all geometry types

Not supported for writing

ClickHouse TypeNotes
DynamicThrows NotImplementedException
AggregateFunctionThrows AggregateFunctionException

Nested type handling

ClickHouse nested types (Nested(...)) can be read and written using array semantics.

CREATE TABLE test.nested (
    id UInt32,
    params Nested (param_id UInt8, param_val String)
) ENGINE = Memory
var row1 = new object[] { 1, new[] { 1, 2, 3 }, new[] { "v1", "v2", "v3" } };
var row2 = new object[] { 2, new[] { 4, 5, 6 }, new[] { "v4", "v5", "v6" } };

await client.InsertBinaryAsync(
    "test.nested",
    new[] { "id", "params.param_id", "params.param_val" },
    new[] { row1, row2 }
);

Logging and diagnostics

The ClickHouse .NET client integrates with the Microsoft.Extensions.Logging abstractions to offer lightweight, opt-in logging. When enabled, the driver emits structured messages for connection lifecycle events, command execution, transport operations, and bulk insert operations. Logging is entirely optional—applications that do not configure a logger continue to run without additional overhead.

Quick start

using ClickHouse.Driver;
using Microsoft.Extensions.Logging;

var loggerFactory = LoggerFactory.Create(builder =>
{
    builder
        .AddConsole()
        .SetMinimumLevel(LogLevel.Information);
});

var settings = new ClickHouseClientSettings("Host=localhost;Port=8123")
{
    LoggerFactory = loggerFactory
};

using var client = new ClickHouseClient(settings);

Using appsettings.json

You can configure logging levels using standard .NET configuration:

using ClickHouse.Driver;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

var configuration = new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile("appsettings.json")
    .Build();

var loggerFactory = LoggerFactory.Create(builder =>
{
    builder
        .AddConfiguration(configuration.GetSection("Logging"))
        .AddConsole();
});

var settings = new ClickHouseClientSettings("Host=localhost;Port=8123")
{
    LoggerFactory = loggerFactory
};

using var client = new ClickHouseClient(settings);

Using in-memory configuration

You can also configure logging verbosity by category in code:

using ClickHouse.Driver;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

var categoriesConfiguration = new Dictionary<string, string>
{
    { "LogLevel:Default", "Warning" },
    { "LogLevel:ClickHouse.Driver.Connection", "Information" },
    { "LogLevel:ClickHouse.Driver.Command", "Debug" }
};

var config = new ConfigurationBuilder()
    .AddInMemoryCollection(categoriesConfiguration)
    .Build();

using var loggerFactory = LoggerFactory.Create(builder =>
{
    builder
        .AddConfiguration(config)
        .AddSimpleConsole();
});

var settings = new ClickHouseClientSettings("Host=localhost;Port=8123")
{
    LoggerFactory = loggerFactory
};

using var client = new ClickHouseClient(settings);

Categories and emitters

The driver uses dedicated categories so that you can fine-tune log levels per component:

CategorySourceHighlights
ClickHouse.Driver.ConnectionClickHouseConnectionConnection lifecycle, HTTP client factory selection, connection opening/closing, session management.
ClickHouse.Driver.CommandClickHouseCommandQuery execution start/completion, timing, query IDs, server statistics, and error details.
ClickHouse.Driver.TransportClickHouseConnectionLow-level HTTP streaming requests, compression flags, response status codes, and transport failures.
ClickHouse.Driver.ClientClickHouseClientBinary insert, queries, and other operations
ClickHouse.Driver.NetTraceTraceHelperNetwork tracing, only when debug mode is enabled

Example: Diagnosing connection issues

{
    "Logging": {
        "LogLevel": {
            "ClickHouse.Driver.Connection": "Trace",
            "ClickHouse.Driver.Transport": "Trace"
        }
    }
}

This will log:

  • HTTP client factory selection (default pool vs single connection)
  • HTTP handler configuration (SocketsHttpHandler or HttpClientHandler)
  • Connection pool settings (MaxConnectionsPerServer, PooledConnectionLifetime, etc.)
  • Timeout settings (ConnectTimeout, Expect100ContinueTimeout, etc.)
  • SSL/TLS configuration
  • Connection open/close events
  • Session ID tracking

Debug mode: network tracing and diagnostics

To help with diagnosing networking issues, the driver library includes a helper that enables low-level tracing of .NET networking internals. To enable it you must pass a LoggerFactory with the level set to Trace, and set EnableDebugMode to true (or manually enable it via the ClickHouse.Driver.Diagnostic.TraceHelper class). Events will be logged to the ClickHouse.Driver.NetTrace category. Warning: this will generate extremely verbose logs, and impact performance. It is not recommended to enable debug mode in production.

var loggerFactory = LoggerFactory.Create(builder =>
{
    builder
        .AddConsole()
        .SetMinimumLevel(LogLevel.Trace); // Must be Trace level to see network events
});

var settings = new ClickHouseClientSettings()
{
    LoggerFactory = loggerFactory,
    EnableDebugMode = true,  // Enable low-level network tracing
};

OpenTelemetry

The driver provides built-in support for OpenTelemetry distributed tracing via the .NET System.Diagnostics.Activity API. When enabled, the driver emits spans for database operations that can be exported to observability backends like Jaeger or ClickHouse itself (via the OpenTelemetry Collector).

Enabling tracing

In ASP.NET Core applications, add the ClickHouse driver's ActivitySource to your OpenTelemetry configuration:

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .AddSource(ClickHouseDiagnosticsOptions.ActivitySourceName)  // Subscribe to ClickHouse driver spans
        .AddAspNetCoreInstrumentation()
        .AddOtlpExporter());             // Or AddJaegerExporter(), etc.

For console applications, testing, or manual setup:

using OpenTelemetry;
using OpenTelemetry.Trace;

var tracerProvider = Sdk.CreateTracerProviderBuilder()
    .AddSource(ClickHouseDiagnosticsOptions.ActivitySourceName)
    .AddConsoleExporter()
    .Build();

Span attributes

Each span includes standard OpenTelemetry database attributes plus ClickHouse-specific query statistics that can be used for debugging.

AttributeDescription
db.systemAlways "clickhouse"
db.nameDatabase name
db.userUsername
db.statementSQL query (if enabled)
db.clickhouse.read_rowsRows read by the query
db.clickhouse.read_bytesBytes read by the query
db.clickhouse.written_rowsRows written by the query
db.clickhouse.written_bytesBytes written by the query
db.clickhouse.elapsed_nsServer-side execution time in nanoseconds

Configuration options

Control tracing behavior via ClickHouseDiagnosticsOptions:

using ClickHouse.Driver.Diagnostic;

// Include SQL statements in spans (default: false for security)
ClickHouseDiagnosticsOptions.IncludeSqlInActivityTags = true;

// Truncate long SQL statements (default: 1000 characters)
ClickHouseDiagnosticsOptions.StatementMaxLength = 500;
Note

Enabling IncludeSqlInActivityTags may expose sensitive data in your traces. Use with caution in production environments.

TLS configuration

When connecting to ClickHouse over HTTPS, you can configure TLS/SSL behavior in several ways.

Custom certificate validation

For production environments requiring custom certificate validation logic, provide your own HttpClient with a configured ServerCertificateCustomValidationCallback handler:

using System.Net;
using System.Net.Security;
using ClickHouse.Driver;

var handler = new HttpClientHandler
{
    // Required when compression is enabled (default)
    AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,

    ServerCertificateCustomValidationCallback = (message, cert, chain, sslPolicyErrors) =>
    {
        // Example: Accept a specific certificate thumbprint
        if (cert?.Thumbprint == "YOUR_EXPECTED_THUMBPRINT")
            return true;

        // Example: Accept certificates from a specific issuer
        if (cert?.Issuer.Contains("YourOrganization") == true)
            return true;

        // Default: Use standard validation
        return sslPolicyErrors == SslPolicyErrors.None;
    },
};

var httpClient = new HttpClient(handler) { Timeout = TimeSpan.FromMinutes(5) };

var settings = new ClickHouseClientSettings
{
    Host = "my.clickhouse.server",
    Protocol = "https",
    HttpClient = httpClient,
};

using var client = new ClickHouseClient(settings);
Note

Important considerations when providing a custom HttpClient

  • Automatic decompression: You must enable AutomaticDecompression if compression is not disabled (compression is enabled by default).
  • Idle timeout: Set PooledConnectionIdleTimeout smaller than the server's keep_alive_timeout (10 seconds for ClickHouse Cloud) to avoid connection errors from half-open connections.

ORM support

ORMs require the ADO.NET API (ClickHouseConnection). For proper connection lifetime management, create connections from a ClickHouseDataSource:

// Register DataSource as singleton
var dataSource = new ClickHouseDataSource("Host=localhost;Username=default");

// Create connections for ORM use
await using var connection = await dataSource.OpenConnectionAsync();
// Pass connection to your ORM...

Dapper

ClickHouse.Driver can be used with Dapper, but anonymous objects are not supported.

Working example:

connection.QueryAsync<string>(
    "SELECT {p1:Int32}",
    new Dictionary<string, object> { { "p1", 42 } }
);

Not supported:

connection.QueryAsync<string>(
    "SELECT {p1:Int32}",
    new { p1 = 42 }
);

Linq2db

This driver is compatible with linq2db, a lightweight ORM and LINQ provider for .NET. See the project website for detailed documentation.

Example usage:

Create a DataConnection using the ClickHouse provider:

using LinqToDB;
using LinqToDB.Data;
using LinqToDB.DataProvider.ClickHouse;

var connectionString = "Host=localhost;Port=8123;Database=default";
var options = new DataOptions()
    .UseClickHouse(connectionString, ClickHouseProvider.ClickHouseDriver);

await using var db = new DataConnection(options);

Table mappings can be defined using attributes or fluent configuration. If your class and property names match the table and column names exactly, no configuration is needed:

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

Querying:

await using var db = new DataConnection(options);

var products = await db.GetTable<Product>()
    .Where(p => p.Price > 100)
    .OrderByDescending(p => p.Name)
    .ToListAsync();

Bulk Copy:

Use BulkCopyAsync for efficient bulk inserts.

await using var db = new DataConnection(options);
var table = db.GetTable<Product>();

var options = new BulkCopyOptions
{
    MaxBatchSize = 100000,
    MaxDegreeOfParallelism = 1,
    WithoutSession = true
};

await table.BulkCopyAsync(options, products);

Entity framework core

Entity Framework Core is currently not supported.

Limitations

AggregateFunction columns

Columns of type AggregateFunction(...) cannot be queried or inserted directly.

To insert:

INSERT INTO t VALUES (uniqState(1));

To select:

SELECT uniqMerge(c) FROM t;