Driver FixRecommendedSound, Wi-Fi or graphics acting up? Check drivers firstFind missing or outdated drivers fast.Check DriversOctober DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsClean PCRecommendedOne scan can reveal what keeps slowing WindowsLook for cleanup and repair opportunities.Run Scan×
Skip to content
World desk6 min

Stop Repeating ClickHouse Columns: Generate Simple DDL from a Pydantic v2 Model

A Pydantic v2 model can drive a small ClickHouse DDL generator, but it cannot choose your storage design. See a bounded example and when SQLAlchemy is a better fit.
Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

For a small Python application, a Pydantic v2 model can be the source of truth for a bounded set of ClickHouse column names and types. A short generator can turn its declared fields into a column list, then pass the resulting CREATE TABLE statement to ClickHouse Connect. The model does not, by itself, define a complete ClickHouse table: your code still needs an explicit type-mapping policy, safe identifier handling, and choices such as the engine and ordering key.

How do I create a ClickHouse table from a Pydantic model?

Inspect the model class’s declared fields, map only types your application explicitly supports, and combine the resulting column definitions with table settings supplied by the caller. Then execute the SQL using ClickHouse Connect’s client.command(...) API. ClickHouse’s Python integration guide shows this command-based approach to table creation; the driver’s API documentation covers commands, DDL, and parameter binding.

As an Amazon Associate I earn from qualifying purchases.

Here is a deliberately small example. It supports only int, str, bool, and nullable versions of those scalar types. The selected ClickHouse types are an application policy, not a universal conversion rule. Confirm that the mappings suit your data and ClickHouse version before using the statement.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
from typing import Optional, Union, get_args, get_origin

from pydantic import BaseModel


def quote_identifier(name: str) -> str:
    # This example accepts simple identifiers only; it rejects rather than
    # attempting to escape arbitrary SQL identifier input.
    if not name or not (name[0].isalpha() or name[0] == "_"):
        raise ValueError(f"Invalid identifier: {name!r}")
    if not all(ch.isalnum() or ch == "_" for ch in name):
        raise ValueError(f"Invalid identifier: {name!r}")
    return f"`{name}`"


def clickhouse_type(annotation: object) -> str:
    nullable = False
    origin = get_origin(annotation)

    # Handle Optional[T] and T | None, but not arbitrary unions.
    if origin in (Union, getattr(__import__("types"), "UnionType")):
        args = get_args(annotation)
        non_none = tuple(arg for arg in args if arg is not type(None))
        if len(non_none) != 1 or len(args) != 2:
            raise TypeError(f"Unsupported union: {annotation!r}")
        annotation = non_none[0]
        nullable = True

    mapping = {int: "Int64", str: "String", bool: "Bool"}
    try:
        result = mapping[annotation]
    except (KeyError, TypeError):
        raise TypeError(f"Unsupported field type: {annotation!r}") from None

    return f"Nullable({result})" if nullable else result


def create_table_sql(
    model: type[BaseModel],
    *,
    table: str,
    engine: str,
    order_by: str,
) -> str:
    if not engine.strip():
        raise ValueError("Specify a ClickHouse engine")

    columns = []
    for name, field in model.model_fields.items():
        column = quote_identifier(name)
        ch_type = clickhouse_type(field.annotation)
        columns.append(f"    {column} {ch_type}")

    # Engine and ORDER BY are SQL fragments, not values. Supply them from
    # trusted application configuration; do not accept arbitrary user input.
    return (
        f"CREATE TABLE {quote_identifier(table)} (n"
        + ",n".join(columns)
        + f"n) ENGINE = {engine} ORDER BY ({quote_identifier(order_by)})"
    )

# Example: sql = create_table_sql(Event, table="events",
#                                 engine="MergeTree", order_by="event_id")
# client.command(sql)

The example uses Pydantic v2’s public model_fields interface to read class-level field definitions; it does not instantiate a record or infer a schema from one record’s values. Pydantic documents its type and customization APIs in its types guide. The generator intentionally raises an error for unsupported types instead of silently treating them as strings.

Important: treat engine as trusted, reviewed SQL configuration. The sample validates table and column identifiers by allowing only simple letters, digits, and underscores; adapt that rule only with deliberate identifier quoting. Parameter binding is for values and does not make arbitrary SQL fragments or interpolated identifiers safe.

What the model does—and does not—decide

A Pydantic annotation describes application data and validation. It is not a complete ClickHouse storage specification. The generator’s supported mappings are merely the contract of this example; do not infer that every Python type has one obvious database equivalent.

  • Nullability: this example wraps supported optional scalar types in Nullable(...). Decide deliberately whether missing or nullable application values should be stored as nullable columns.
  • Numeric representation: int is mapped here to Int64, but a different range or storage choice may require a different ClickHouse type. Floating-point values and decimal precision need explicit decisions.
  • Complex and temporal values: arrays, nested models, enums, dates and times, custom classes, and arbitrary generic types are rejected. Supporting them requires explicit mapping and testing, including choices about temporal precision and time zones.
  • Column behavior: defaults, aliases, codecs, materialized or alias columns, and other ClickHouse-specific behavior are outside this generator’s scope.

For a maintainable extension, define an explicit contract for ClickHouse-specific field metadata—for example, a supported Annotated metadata item or a documented Field extra—and validate it when generating DDL. Pydantic recommends high-level customization constructs such as Annotated and Field; its documentation does not prescribe a universal Pydantic-to-ClickHouse conversion table.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Why engine and ordering choices still need a human decision

Column types alone do not make a useful ClickHouse table definition. The engine and ordering key affect how the table is organized and queried; partitioning, defaults, and other table properties may matter to the application too. ClickHouse’s Python integration and driver examples show table DDL with engine and ordering clauses, rather than deriving those settings from ordinary Python scalar annotations.

Make those decisions visible in the generator’s inputs or in a separate, reviewed table configuration. Avoid pretending that a Pydantic class can infer the correct engine or ordering key from field names. The example requires the caller to provide both, and it is intentionally limited to a basic ENGINE and ORDER BY form.

Do you need SQLAlchemy to create a ClickHouse table in Python?

No. If the only goal is to generate a small, predictable column list from a validation model, a focused generator plus ClickHouse Connect may be enough. Choose an existing SQLAlchemy route when you need its metadata, migration, or schema-management capabilities rather than just a CREATE TABLE string.

Approach Best fit Trade-off or caveat
Pydantic-driven generator with ClickHouse Connect A focused application wants validation models and simple table-creation SQL without introducing ORM metadata. Your application owns type mapping, identifier rules, and ClickHouse-specific table policy. ClickHouse Connect provides DDL execution, not a built-in Pydantic generator.
ClickHouse Connect SQLAlchemy dialect A project already uses SQLAlchemy Core or wants Alembic migration support. The ClickHouse Connect repository describes a lightweight dialect and explicitly says full ORM support is not provided.
clickhouse-sqlalchemy A project wants declarative table definitions with ClickHouse types and engine constructs. Its documentation describes release 0.3.2 and SQLAlchemy 1.4 support. Check the current project state and compatibility with your stack before adopting it.

For either SQLAlchemy option, assess the actual features your project needs rather than assuming that a dialect implies comprehensive ORM support. The ClickHouse Connect repository documents Core and Alembic capabilities along with ORM limitations; the separate clickhouse-sqlalchemy documentation covers declarative models and generated DDL.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

What to check before using generated DDL

  1. Review the mapping contract. Confirm each supported Python annotation maps to the intended ClickHouse type, including null behavior and numeric range.
  2. Reject what you have not implemented. Keep nested models, arbitrary unions, custom types, and unsupported metadata as errors until the generator handles them explicitly.
  3. Validate SQL identifiers and configuration. Keep table and column names within a documented rule. Treat engine and ordering expressions as trusted configuration, not user-supplied SQL.
  4. Review the complete table design. Set engine and ordering deliberately; add partitioning, defaults, or other ClickHouse features only through explicit, reviewed configuration.
  5. Check the target deployment. Validate the generated statement against the ClickHouse version and environment where it will run before applying it. A successful Pydantic model definition does not establish that its generated DDL is valid for every deployment.

One additional v2 pitfall: do not copy Pydantic v1 schema-customization examples that rely on __modify_schema__. Pydantic’s migration documentation says that hook is unsupported in v2 and points custom JSON Schema implementations to __get_pydantic_json_schema__. JSON Schema customization is not itself a ClickHouse type-mapping mechanism; keep database mapping explicit.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

More from the Wire

  1. World desk4 min
    How to Spot an AI Voice Scam Before Sending MoneyDon’t rely on how a caller sounds. Pause, call back through a known number, and verify the emergency with another trusted person before sending money.
  2. Mountain View desk4 min
    Google’s SynthID Detector: How to Check AI-Generated Images, Video and AudioGoogle’s SynthID Detector looks for an embedded watermark in supported images, video and audio. Here is what its results do—and do not—show.
  3. Redmond desk20 min
    How to create a link to File or Folder in Windows 11Windows 11 gives you several ways to point to a file or folder without moving or duplicating it. You can create a desktop shortcut,…
Recommended PC Tool
Recommended PC Tool
PC Slower Than It Used to Be?Free scan - under a minute
Crashes, No Sound, or Screen Glitches?Free driver scan

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.