Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
65c361f
simple case
peter-leonov-ch May 22, 2026
d600555
wip
peter-leonov-ch May 27, 2026
c5614eb
EXPLAIN AST json=1: named slots for ASTFunction
peter-leonov-ch Jun 9, 2026
069fc52
EXPLAIN AST json=1: named slots for ASTOrderByElement
peter-leonov-ch Jun 9, 2026
bbcdc56
EXPLAIN AST json=1: named slots for ASTSelectQuery
peter-leonov-ch Jun 9, 2026
182cf5c
EXPLAIN AST json=1: document the named-slots design in AST2.md
peter-leonov-ch Jun 10, 2026
7b3ccf3
EXPLAIN AST json=1: keep children for ASTSelectIntersectExceptQuery
peter-leonov-ch Jun 10, 2026
66f2e04
EXPLAIN AST json=1: tests for expressions and set operations
peter-leonov-ch Jun 10, 2026
93506b2
EXPLAIN AST json=1: full-AST showcase test
peter-leonov-ch Jun 10, 2026
b433bd4
EXPLAIN AST json=1: named slots for the structural wrapper nodes
peter-leonov-ch Jun 10, 2026
3243ccd
EXPLAIN AST json=1: test for structural wrapper named slots
peter-leonov-ch Jun 10, 2026
66a920f
EXPLAIN AST json=1: named slots for the remaining leaf nodes
peter-leonov-ch Jun 10, 2026
2ea1126
EXPLAIN AST json=1: consolidate design notes into AST.md
peter-leonov-ch Jun 10, 2026
520c71d
EXPLAIN AST json=1: add SQL -> JSON-AST fixture corpus
peter-leonov-ch Jun 10, 2026
d5175f3
EXPLAIN AST json=1: harvester to reuse the stateless suite as a corpus
peter-leonov-ch Jun 10, 2026
78b5c53
EXPLAIN AST json=1: AST-shape dedup + committed depth-3 shape corpus
peter-leonov-ch Jun 10, 2026
c689186
EXPLAIN AST json=1: named slots for the CREATE-TABLE family and INSERT
peter-leonov-ch Jun 10, 2026
d1d52b4
EXPLAIN AST json=1: regenerate depth-3 shape corpus after DDL enrichment
peter-leonov-ch Jun 10, 2026
48da7ab
EXPLAIN AST json=1: named slots for CREATE sub-elements and DML/table…
peter-leonov-ch Jun 10, 2026
c24fa4f
EXPLAIN AST json=1: named slots for the ALTER family and dictionaries
peter-leonov-ch Jun 10, 2026
22a6292
EXPLAIN AST json=1: regenerate depth-3 shape corpus after ALTER/dict …
peter-leonov-ch Jun 10, 2026
57d6b26
EXPLAIN AST json=1: named slots for the long-tail statements
peter-leonov-ch Jun 10, 2026
fe12226
EXPLAIN AST json=1: regenerate depth-3 shape corpus after long-tail e…
peter-leonov-ch Jun 10, 2026
2f6b22b
EXPLAIN AST json=1: give dictionary sub-elements distinct type ids
peter-leonov-ch Jun 10, 2026
ad33aa4
EXPLAIN AST json=1: regenerate shape corpus after dictionary type-id fix
peter-leonov-ch Jun 10, 2026
06e293d
EXPLAIN AST json=1: freeze the format for external consumers
peter-leonov-ch Jun 12, 2026
52a17a6
EXPLAIN AST json=1: regenerate fixture corpus for the frozen format
peter-leonov-ch Jun 12, 2026
27b4807
EXPLAIN AST json=1: AST.md intro mentions formatASTAsJSONDocument
peter-leonov-ch Jun 14, 2026
3c699d4
EXPLAIN AST json=1: document the clickhouse-js-parser sync in AST.md
peter-leonov-ch Jun 14, 2026
61f8adf
EXPLAIN AST json=1: emit enum values and tuple element names (format v2)
peter-leonov-ch Jun 22, 2026
26f739a
EXPLAIN AST json=1: stateless tests for the v2 data-type element slots
peter-leonov-ch Jun 22, 2026
9c6d8cc
EXPLAIN AST json=1: regenerate fixture corpus for format v2
peter-leonov-ch Jun 22, 2026
ad4410a
datatype-parser: standalone, portable parser for data-type strings
peter-leonov-ch Jun 22, 2026
c8329a6
datatype-parser: reject malformed numeric literals at value parsing
peter-leonov-ch Jun 27, 2026
e418661
make JSON AST more complete
pulpdrew Jul 2, 2026
2ff5b16
Merge pull request #2 from pulpdrew/drew/json_ast_additions
peter-leonov-ch Jul 3, 2026
47cd8c3
attach types to NamedCollection and Workload KV Pairs
pulpdrew Jul 6, 2026
ec30e77
Merge pull request #3 from pulpdrew/drew/json_ast_additions
peter-leonov-ch Jul 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
The diff you're trying to view is too large. We only load the first 3000 changed files.
761 changes: 761 additions & 0 deletions AST.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions datatype-parser/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
build/
27 changes: 27 additions & 0 deletions datatype-parser/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
cmake_minimum_required(VERSION 3.16)
project(chdt_datatype_parser CXX)

# A self-contained library: no dependency on the ClickHouse source tree.
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release)
endif()

add_compile_options(-Wall -Wextra)

add_library(chdt_datatype_parser
src/lexer.cpp
src/parser.cpp
src/json.cpp
)
target_include_directories(chdt_datatype_parser PUBLIC include)

# CLI: read a data-type string, print its JSON AST. Exit non-zero on error.
add_executable(chdt-parse tool/main.cpp)
target_link_libraries(chdt-parse PRIVATE chdt_datatype_parser)

enable_testing()
add_subdirectory(test)
107 changes: 107 additions & 0 deletions datatype-parser/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# chdt — standalone ClickHouse data-type parser

A small, self-contained C++ library that parses a ClickHouse **data-type
string** (the kind sent in the types row of `RowBinaryWithNamesAndTypes`, e.g.
`Array(Nullable(UInt64))`, `Tuple(a UInt8, b String)`, `Enum8('a' = 1)`,
`Decimal(10, 2)`) into a JSON AST.

It is extracted from the server's `ParserDataType`
(`src/Parsers/ParserDataType.cpp`) but has **no dependency on the ClickHouse
source tree** — only the C++20 standard library. The JSON it emits mirrors the
data-type subtree of the frozen `EXPLAIN AST json = 1` document (format
**version 2**; see `AST.md` in the ClickHouse repo), so its output is a drop-in
match for what the server produces.

## Why this exists

The server's type parser is entangled with the lexer, the expression parsers,
and the `IAST` / `Field` machinery. Vendoring all of that verbatim would pull
in ~9–10k lines (`Field`, `ReadHelpers`, `Exception`, the formatting/hashing
layer). Instead, this reimplements just the type grammar on a minimal AST:

- a purpose-built tokenizer (`src/lexer.*`) covering the slice of SQL that type
strings use, in place of the full `Lexer` and its `UTF8Helpers` /
`find_symbols` dependencies;
- a faithful port of `ParserDataType::parseImpl` (`src/parser.cpp`) — same
control flow: identifier + SQL-standard multi-word aliases, the Enum and
Tuple special cases, then the generic parametric-argument loop;
- plain structs for the AST (`include/chdt/ast.h`) instead of `IAST` + `Field`.

## Building

```bash
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
```

This produces `libchdt_datatype_parser.a` and the `chdt-parse` CLI.

## Usage

Library:

```cpp
#include "chdt/parser.h"

chdt::ParseResult r = chdt::parseDataType("Tuple(a UInt8, b String)");
if (r.ok())
std::string json = chdt::toJSON(*r.ast);
else
/* r.error->message, r.error->position */;
```

CLI:

```bash
./build/chdt-parse "Array(Nullable(UInt64))"
echo "Enum8('a' = 1, 'b' = 2)" | ./build/chdt-parse
```

## Output shape

Node types and slots match the server (format v2):

| `type` | slots |
|------------------|----------------------------------------------------------|
| `DataType` | `name`, `arguments?` (present iff the type had `(...)`) |
| `EnumDataType` | `name`, `values` (array of `{ name, value }`) |
| `TupleDataType` | `name`, `arguments?`, `element_names?` (named tuples) |
| `NameTypePair` | `name`, `data_type` (a `Nested(...)` element) |
| `Literal` | `value_type`, `value` (64-bit ints as JSON strings) |
| `Function` | `name`, `is_operator?`, `arguments` (e.g. `max_types=5`) |
| `Identifier` | `name`, `name_parts?` |

`EnumDataType.values` and `TupleDataType.element_names` are carried here exactly
as the server emits them since format v2.

## Coverage

Supported: scalars, parametric types with literal args (`Decimal`,
`FixedString`, `DateTime64`, …), nested type args (`Array`, `Map`, `Nullable`,
`LowCardinality`, `Variant`, …), enums (explicit → `EnumDataType`;
auto-assigned → generic `DataType`), named/unnamed/mixed tuples, `Nested`,
`Dynamic(max_types = N)`, the legacy `Object('json')`, and the SQL-standard
multi-word aliases (`DOUBLE PRECISION`, `CHAR VARYING`, `INT SIGNED`, …).

**Deliberately not supported yet** (the parser returns a clear error):

- `AggregateFunction` / `SimpleAggregateFunction` — needs the function-expression
parser the server reaches for here.
- the new `JSON(...)` object-argument syntax (`JSON(a.b UInt32, SKIP x)`). Bare
`JSON` and legacy `Object('json')` parse fine.

## Tests

`ctest` runs two suites against the parser:

- **oracle** (`test/oracle_compare.py`) — for each type in `test/cases.txt`,
compares the parser's JSON against the `data_type` subtree the real server
emits for `CREATE TABLE t (c <TYPE>) ENGINE = Null`. Needs a `clickhouse`
binary (default: `../build/programs/clickhouse`; override with
`-DCLICKHOUSE_BINARY=...`).
- **unsupported** (`test/check_unsupported.py`) — asserts the deferred types in
`test/cases_unsupported.txt` are rejected.

```bash
ctest --test-dir build --output-on-failure
```
87 changes: 87 additions & 0 deletions datatype-parser/include/chdt/ast.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#pragma once

/// Minimal, self-contained AST for ClickHouse data-type strings.
///
/// The node shapes mirror the frozen `EXPLAIN AST json = 1` document
/// (format version 2; see ClickHouse `AST.md`) so that JSON produced here is
/// a drop-in match for the data-type subtree the server emits — and a
/// superset of it: `EnumDataType.values` and `TupleDataType.element_names`
/// are carried here as they are in the server (since v2).
///
/// This header has no dependency on the ClickHouse source tree.

#include <cstdint>
#include <memory>
#include <string>
#include <vector>

namespace chdt
{

enum class NodeKind
{
DataType, /// generic type: name + optional argument list
EnumDataType, /// Enum / Enum8 / Enum16 with fully explicit values
TupleDataType, /// Tuple, with optional element names
NameTypePair, /// `name Type` element of a Nested(...)
Literal, /// numeric / string argument (e.g. Decimal(10, 2))
Function, /// operator/function argument (e.g. `max_types = 5`)
Identifier, /// bare identifier argument
};

struct Node;
using NodePtr = std::shared_ptr<Node>;

struct EnumValue
{
std::string name;
int64_t value = 0;
};

/// One node type for the whole tree. Only the fields relevant to `kind` are
/// populated; serialization emits exactly the slots the server would.
struct Node
{
explicit Node(NodeKind kind_) : kind(kind_) {}

NodeKind kind;

/// DataType / EnumDataType / TupleDataType / Function / Identifier / NameTypePair
std::string name;

/// DataType / TupleDataType / Function argument list (children inlined in JSON).
std::vector<NodePtr> arguments;
/// DataType only: whether the type carried a parenthesised argument list at
/// all. `UInt8` omits the `arguments` slot; `Array(...)` emits it (possibly
/// empty). Tuple/Function always emit their list.
bool has_argument_list = false;

/// EnumDataType: explicit `'name' = value` pairs.
std::vector<EnumValue> values;

/// TupleDataType: element names. Empty => unnamed tuple (slot omitted).
std::vector<std::string> element_names;

/// NameTypePair: the element's type.
NodePtr data_type;

/// Literal: `value_type` is the Field type id ("UInt64", "Int64",
/// "Float64", "String"); `value` is the textual value.
std::string value_type;
std::string value;

/// Function: set for operators such as `equals`.
bool is_operator = false;

/// Identifier: populated when the identifier is compound (a.b).
std::vector<std::string> name_parts;

static NodePtr make(NodeKind kind_) { return std::make_shared<Node>(kind_); }
};

/// Serialize a node tree to JSON, matching the server's `formatASTAsJSON`
/// shape for data types. `indent` < 0 produces compact output; >= 0 produces
/// pretty output with that many spaces per level.
std::string toJSON(const Node & node, int indent = 2);

}
44 changes: 44 additions & 0 deletions datatype-parser/include/chdt/parser.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#pragma once

/// Public entry point: parse a ClickHouse data-type string into the AST in
/// `ast.h`. Self-contained — no dependency on the ClickHouse source tree.
///
/// Coverage mirrors the server's `ParserDataType` (`src/Parsers/ParserDataType.cpp`)
/// with two deliberate omissions, deferred for now:
/// * AggregateFunction / SimpleAggregateFunction — would pull in the full
/// function-expression parser; a clear error is returned instead.
/// * the new JSON/Object path-typed arguments (`JSON(a.b UInt32, SKIP x)`).
/// The bare `JSON` type and legacy `Object('json')` parse fine; the
/// object-argument syntax returns an error.
/// Everything else — nested types, parametric types, enums (explicit and
/// auto-assigned), named/unnamed tuples, Nested, Dynamic(max_types=N), and the
/// SQL-standard multi-word aliases — is supported.

#include <optional>
#include <string>

#include "chdt/ast.h"

namespace chdt

{

struct ParseError
{
std::string message; /// human-readable description
size_t position = 0; /// byte offset into the input where parsing stuck
};

struct ParseResult
{
NodePtr ast; /// non-null on success
std::optional<ParseError> error; /// set on failure

bool ok() const { return ast != nullptr; }
};

/// Parse the whole string as a single data type. Trailing tokens after a
/// complete type are an error (the entire input must be one type).
ParseResult parseDataType(const std::string & input);

}
Loading