diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 5dd58e1..987aaa2 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -14,7 +14,7 @@ jobs:
- uses: actions/checkout@v4
- uses: mlugg/setup-zig@v2
with:
- version: 0.16.0
+ version: 0.17.1
- name: zig build test
run: zig build test --summary all
- name: zig build audit
@@ -43,7 +43,7 @@ jobs:
- uses: actions/checkout@v4
- uses: mlugg/setup-zig@v2
with:
- version: 0.16.0
+ version: 0.17.1
- name: Wait for ClickHouse
run: |
for i in $(seq 1 30); do
@@ -113,3 +113,7 @@ jobs:
run: zig build smoke -- lc-write
- name: branch-readiness smoke aggregate
run: zig build smoke-readiness
+ - name: smoke async (cancellation + concurrency)
+ run: zig build smoke-async -- all
+ - name: example 06 concurrent queries
+ run: zig build run-06-concurrent-queries
diff --git a/.serena/.gitignore b/.serena/.gitignore
new file mode 100644
index 0000000..2e510af
--- /dev/null
+++ b/.serena/.gitignore
@@ -0,0 +1,2 @@
+/cache
+/project.local.yml
diff --git a/.serena/project.yml b/.serena/project.yml
new file mode 100644
index 0000000..dd2322c
--- /dev/null
+++ b/.serena/project.yml
@@ -0,0 +1,169 @@
+# the name by which the project can be referenced within Serena/when chatting with the LLM.
+project_name: "clickzig"
+
+# list of language servers to start when using the LSP backend; choose from:
+# ada al angular ansible bash
+# bsl clojure cpp cpp_ccls crystal
+# csharp csharp_omnisharp cue dart deno
+# elixir elm erlang fortran fsharp
+# gdscript gleam go groovy haskell
+# haxe hlsl html java json
+# julia kotlin latex lean4 lua
+# luau markdown matlab msl nextflow
+# nix ocaml pascal perl php
+# php_phpactor php_phpantom powershell python python_basedpyright
+# python_jedi python_pyrefly python_ty qml r
+# rego ruby ruby_solargraph rust scala
+# scss solidity svelte swift systemverilog
+# terraform toml typescript typescript_vts vue
+# wolfram yaml zig
+# (This list may be outdated; generated with scripts/print_language_list.py;
+# For the current list, see values of the LanguageServerId enum here:
+# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py)
+# For some languages, there are several alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
+# Note:
+# - For C, use cpp
+# - For JavaScript, use typescript
+# - For Angular projects, use angular (subsumes typescript+html; requires `npm install` in the project root)
+# - For Svelte projects, use svelte (subsumes typescript/javascript for .svelte projects; requires npm)
+# - For Deno projects, use deno (serves the same .ts/.js files as typescript; requires the deno CLI on PATH)
+# - For SCSS / Sass / plain CSS, use scss (some-sass-language-server handles all three)
+# - For Free Pascal/Lazarus, use pascal
+# Special requirements:
+# Some language servers require additional setup/installations.
+# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers
+# When using multiple language servers, the first language server that supports a given file will be used for that file.
+# The first language server is the default language and the respective language server will be used as a fallback.
+# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
+language_servers:
+- zig
+
+# the encoding used by text files in the project
+# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
+encoding: "utf-8"
+
+# optional shell command to run before the language backend (LSP or JetBrains) is initialised.
+# the command runs in the project root directory and is only executed if the project is trusted
+# (see trusted_project_path_patterns in the global configuration).
+# serena waits for the command to exit: a non-zero exit code is logged as an error but does not
+# abort activation. a per-project timeout (activation_command_timeout, default 180s) is the safety
+# backstop for non-terminating commands; on expiry the process is killed and activation continues.
+# example: activation_command: "npx nx run-many -t build"
+activation_command:
+
+# maximum time in seconds to wait for activation_command to complete before killing it (default 180s).
+# must be a positive number.
+activation_command_timeout: 180.0
+
+# line ending convention to use when writing source files.
+# Possible values: unset (use global setting), "lf", "crlf", or "native" (platform default)
+# This does not affect Serena's own files (e.g. memories and configuration files), which always use native line endings.
+line_ending:
+
+# The language backend to use for this project.
+# If not set, the global setting from serena_config.yml is used.
+# Valid values: LSP, JetBrains
+# Note: the backend is fixed at startup. If a project with a different backend
+# is activated post-init, an error will be returned.
+language_backend:
+
+# whether to use project's .gitignore files to ignore files
+ignore_all_files_in_gitignore: true
+
+# advanced configuration option allowing to configure language server-specific options.
+# Maps the language key to the options.
+# The settings are considered only if the project is trusted (see global configuration to define trusted projects).
+# See https://oraios.github.io/serena/02-usage/050_configuration.html#language-server-specific-settings
+ls_specific_settings: {}
+
+# list of workspace folder paths (LSP backend only).
+# These folders will be used to build up Serena's symbol index.
+# Paths must be within the project root and should thus be relative to the project root.
+# Furthermore, the paths should not be filtered by ignore settings.
+# Default setting: The entire project root folder (".") is considered.
+# In (large) monorepos, this can be used to index only subfolders of the project root, e.g.
+# ls_workspace_folders:
+# - "./subproject1"
+# - "./subproject2"
+ls_workspace_folders:
+- "."
+
+# list of additional workspace folder paths for cross-package reference support.
+# Paths can be absolute or relative to the project root.
+# Each folder is registered as an LSP workspace folder, enabling language servers to discover
+# symbols and references across package boundaries, but these folders are not indexed by Serena,
+# i.e. the respective symbols will not be found using Serena's symbol search tools.
+# Example:
+# additional_workspace_folders:
+# - ../sibling-package
+# - ../shared-lib
+ls_additional_workspace_folders: []
+
+# list of additional paths to ignore in this project.
+# Same syntax as gitignore, so you can use * and **.
+# Important: quote patterns that start with `*`, otherwise YAML treats them as aliases.
+# Example:
+# ignored_paths:
+# - "examples/**"
+# - ".worktrees/**"
+# - "**/bin/**"
+# - "**/obj/**"
+# Note: global ignored_paths from serena_config.yml are also applied additively.
+ignored_paths: []
+
+# whether the project is in read-only mode
+# If set to true, all editing tools will be disabled and attempts to use them will result in an error
+# Added on 2025-04-18
+read_only: false
+
+# list of tool names to exclude.
+# This extends the existing exclusions (e.g. from the global configuration)
+# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html
+excluded_tools: []
+
+# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default).
+# This extends the existing inclusions (e.g. from the global configuration).
+# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html
+included_optional_tools: []
+
+# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools.
+# This cannot be combined with non-empty excluded_tools or included_optional_tools.
+# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html
+fixed_tools: []
+
+# list of mode names that are to be activated by default, overriding the setting in the global configuration.
+# The full set of modes to be activated is base_modes (from global config) + default_modes + added_modes.
+# If the setting is undefined/empty, the default_modes from the global configuration (serena_config.yml) apply.
+# Otherwise, this overrides the setting from the global configuration (serena_config.yml).
+# Therefore, you can set this to [] if you do not want the default modes defined in the global config to apply
+# for this project.
+# This setting can, in turn, be overridden by CLI parameters (--mode).
+# See https://oraios.github.io/serena/02-usage/050_configuration.html#modes
+default_modes:
+
+# list of mode names to be activated additionally for this project, e.g. ["query-projects"]
+# The full set of modes to be activated is base_modes (from global config) + default_modes + added_modes.
+# See https://oraios.github.io/serena/02-usage/050_configuration.html#modes
+added_modes:
+
+# initial prompt for the project. It will always be given to the LLM upon activating the project
+# (contrary to the memories, which are loaded on demand).
+initial_prompt: ""
+
+# time budget (seconds) per tool call for the retrieval of additional symbol information
+# such as docstrings or parameter information.
+# This overrides the corresponding setting in the global configuration; see the documentation there.
+# If null or missing, use the setting from the global configuration.
+symbol_info_budget:
+
+# list of regex patterns which, when matched, mark a memory entry as read‑only.
+# Extends the list from the global configuration, merging the two lists.
+read_only_memory_patterns: []
+
+# list of regex patterns for memories to completely ignore.
+# Matching memories will not appear in list_memories or activate_project output
+# and cannot be accessed via read_memory or write_memory.
+# To access ignored memory files, use the read_file tool on the raw file path.
+# Extends the list from the global configuration, merging the two lists.
+# Example: ["_archive/.*", "_episodes/.*"]
+ignored_memory_patterns: []
diff --git a/.zigversion b/.zigversion
index 04a373e..7cca771 100644
--- a/.zigversion
+++ b/.zigversion
@@ -1 +1 @@
-0.16.0
+0.17.1
diff --git a/README.md b/README.md
index c59d78c..53778cf 100644
--- a/README.md
+++ b/README.md
@@ -8,22 +8,23 @@
clickzig
-Native ClickHouse TCP for Zig 0.16.0
+Native ClickHouse TCP for Zig 0.17
- Streaming queries, Native INSERT, compression, pooling, TLS, parameters, external data, and broad ClickHouse type coverage.
+ Async-ready over std.Io: streaming queries, Native INSERT, compression, pooling, TLS, parameters, external data, and broad ClickHouse type coverage.
-
+
## at a glance
-- ✅ **Supported for Zig 0.16.0** - `main` and `0.16.0` track the maintained Zig 0.16 line.
+- ✅ **Supported for Zig 0.17** - `main` tracks Zig 0.17; the `0.16.0` branch keeps the Zig 0.16 line.
+- 🔀 **Async via `std.Io`** - run queries concurrently with `io.async` / `io.concurrent` / `Io.Group`; cancel with `Future.cancel`, and clickzig tells the server to stop (protocol `Cancel`) while keeping the connection reusable.
- ⚡ **Native protocol** - talks ClickHouse TCP directly on `9000` or `9440` for TLS.
- 📡 **Streaming queries** - `Client.query()` returns a `ResultStream` over Data, Progress, Logs, Totals, Extremes, Exceptions, and EndOfStream.
- 📥 **Native INSERT** - bulk column-oriented INSERT with caller-owned buffers and explicit type names.
@@ -31,16 +32,17 @@
- 📦 **External tables** - send named Native blocks alongside a query.
- 🗜️ **Compression** - LZ4 and ZSTD read/write frames, opt-in by config or per query.
- 🔐 **TLS + transports** - built-in TCP/TLS transports, plus a vtable for custom backends.
-- 🧵 **Pool** - thread-safe acquire/release with broken-connection discard and lifetime expiry.
+- 🧵 **Pool** - `std.Io`-safe acquire/release (threads or tasks), cancelable waits, broken-connection discard, and lifetime expiry.
- 🧬 **Type coverage** - primitives, Nullable, Array, Tuple, Map, Decimal, LowCardinality, JSON, Dynamic, Sparse, Geo aliases, and common ClickHouse aliases.
- 🛡️ **Security hardening** - allocation caps, timeout mapping, hostile-frame audit probes, and compression checksum validation.
-- 🧪 **CI-backed smoke tests** - live ClickHouse tests cover compression, parameters, external data, pool, timeouts, and combined scenarios.
+- 🏎️ **Fast** - on par with or ahead of the C++ `clickhouse-client` on most read scans; see [benchmarks](#benchmarks).
+- 🧪 **CI-backed smoke tests** - live ClickHouse tests cover compression, parameters, external data, pool, timeouts, cancellation, concurrency, and combined scenarios.
-Versioning follows the Karl Seguin-style Zig package convention: branches target Zig compiler lines, not a normal semver release train. Package metadata is pinned to Zig 0.16.0 (`build.zig.zon` + `.zigversion`). The `0.16.0` branch is the long-lived Zig 0.16 line; a future `dev` branch may track Zig development snapshots.
+Versioning follows the Karl Seguin-style Zig package convention: branches target Zig compiler lines, not a normal semver release train. Package metadata is pinned to Zig 0.17 (`build.zig.zon` + `.zigversion`). The `0.16.0` branch is the long-lived Zig 0.16 line; a future `dev` branch may track Zig development snapshots.
-## intentional non-goals for v0.16.0
+## intentional non-goals
-- **Async via `std.Io` fibers** — current API is sync. Iterator-first stream contract is locked so the column-store decoder doesn't fight the API later.
+- **Multiplexing several queries over one connection** — the ClickHouse native protocol serves one query per connection at a time. Concurrency comes from several connections (`Pool`), not from interleaving on one.
- **SQL placeholder rewriting** — Go-style `?`, `$1`, and `@name` client-side rewriting is intentionally not implemented. Use native ClickHouse `{name:Type}` placeholders through `QueryOptions.parameters`.
## design decisions worth knowing up front
@@ -49,8 +51,9 @@ Versioning follows the Karl Seguin-style Zig package convention: branches target
- **Transport interface, not concrete `Stream`.** `Client` holds a vtable. Built-in `TcpTransport` covers TCP/DNS; alternate backends or test mocks drop in without touching Client code.
- **Iterator-first query shape.** `Client.query()` returns a `ResultStream` with `next() !?Packet`, so callers can stream Data, Progress, Log, Totals, Extremes, and Exception packets without buffering the full result.
- **Lifecycle observability built in.** `Config.on_event` fires at every state transition (connecting, hello_sent, pong_received, ...). Default null = zero overhead. Layer metrics or OTel adapters on top without library changes.
-- **Single-thread per Client.** State machine is non-atomic by design. Use one Client per thread or the built-in `Pool` for multi-threaded workloads.
-- **Cancellation via `*const std.atomic.Value(bool)`.** Polled at every I/O boundary in Client methods. Zig 0.16's `Future.cancel` is async-task-bound; for a sync API the atomic-bool is the right primitive.
+- **`std.Io` all the way down.** Every blocking step (dial, read, write, timeouts, pool waits) goes through the `Io` you pass in, so the same code is synchronous on `Io.Threaded` and concurrent under `io.async` / `io.concurrent` / `Io.Group`. Read/write timeouts use `Io.Batch` operations rather than helper tasks.
+- **One in-flight call per Client.** The state machine is non-atomic by design. Give each concurrent task its own Client, or share a `Pool`.
+- **Cancellation is `Io` cancellation.** `future.cancel(io)` / `group.cancel(io)` surfaces as `error.Canceled`. If the client was waiting for the next server packet it sends a protocol `Cancel`, drains the reply (bounded by `cancel_drain_timeout_ms`), and stays reusable; `ResultStream.cancel()` does the same when you just want to stop reading early.
## install
@@ -60,7 +63,7 @@ Depend on the branch that matches your Zig compiler:
zig fetch --save git+https://github.com//clickzig#main
```
-`main` targets Zig 0.16.0. Use the `0.16.0` branch for the maintained Zig 0.16 line. A future `dev` branch may target Zig development snapshots.
+`main` targets Zig 0.17. Use the `0.16.0` branch for the maintained Zig 0.16 line. A future `dev` branch may target Zig development snapshots.
## quick start: query
@@ -75,13 +78,13 @@ pub fn main(init: std.process.Init) !void {
.write_buffer_size = 4 * 1024,
.username = "default",
.password = "test",
- }, init.io, null, null);
+ }, init.io, null);
defer client.close();
var arena: std.heap.ArenaAllocator = .init(init.gpa);
defer arena.deinit();
- var stream = try client.query("SELECT number, number * 2 FROM numbers(5)", arena.allocator(), null, .{});
+ var stream = try client.query("SELECT number, number * 2 FROM numbers(5)", arena.allocator(), .{});
while (try stream.next()) |packet| switch (packet) {
.data => |b| {
for (0..b.num_rows) |i| std.debug.print("{d} -> {d}\n", .{ b.columns[0].column.UInt64[i], b.columns[1].column.UInt64[i] });
@@ -107,7 +110,6 @@ try client.insert(
3,
&cols,
arena.allocator(),
- null,
.{},
);
```
@@ -125,7 +127,6 @@ try params.putDate(arena.allocator(), "day", "2026-05-07");
var stream = try client.query(
"SELECT {n:UInt64} + 1, {label:String}, toDate({day:String})",
arena.allocator(),
- null,
.{ .parameters = ¶ms },
);
```
@@ -138,11 +139,34 @@ var stream = try client.query(
const pool = try clickzig.Pool.init(init.gpa, init.io, config, .{ .max_size = 16 });
defer pool.deinit();
-const client = try pool.acquire(null);
+const client = try pool.acquire();
defer pool.release(client);
-try client.ping(null);
+try client.ping();
```
+## quick start: async
+
+```zig
+// Fan out over a pool: each task gets its own connection.
+var group: std.Io.Group = .init;
+defer group.cancel(io);
+for (&jobs) |*job| group.async(io, runJob, .{ pool, gpa, job });
+try group.await(io);
+
+// Deadline: cancel a slow query after 500ms. The server stops it too,
+// and the connection goes back to the pool ready for reuse.
+var slow = try io.concurrent(slowQuery, .{ pool, gpa });
+try (std.Io.Clock.Duration{ .raw = .fromMilliseconds(500), .clock = .awake }).sleep(io);
+_ = slow.cancel(io) catch |e| switch (e) {
+ error.Canceled => {},
+ else => return e,
+};
+```
+
+Use `io.concurrent` (not `io.async`) when the caller must keep running while the task waits: `io.async` is allowed to run the task inline. See `examples/06_concurrent_queries.zig` for the full program.
+
+> **Backend note (Zig 0.17.1):** `std.Io.Evented` (io_uring / kqueue) does not implement TCP networking yet — connect, `net_read`, and `net_write` are still `TODO` in std. Use `Io.Threaded`, which runs `async`/`concurrent` tasks on a thread pool and supports cancellation. clickzig only talks to the `Io` interface, so evented backends will work once std implements their networking.
+
## quick start: dsn
```zig
@@ -153,24 +177,50 @@ const result = try clickzig.fromUri(
arena.allocator(),
.{ .control_allocator = init.gpa, .read_buffer_size = 64 * 1024, .write_buffer_size = 4 * 1024 },
);
-const client = try clickzig.Client.connectTcp(result.config, init.io, null, null);
+const client = try clickzig.Client.connectTcp(result.config, init.io, null);
defer client.close();
```
## examples
-Five focused, copy-pasteable examples in `examples/`. Each demonstrates one concept in isolation:
+Six focused, copy-pasteable examples in `examples/`. Each demonstrates one concept in isolation:
| | example | what it shows | run |
|---|---|---|---|
| 01 | `01_connect.zig` | minimal happy path | `zig build run-01-connect` |
| 02 | `02_diagnostics.zig` | catch `error.AuthenticationFailed`, inspect the server's parsed `ServerError` | `zig build run-02-diagnostics` |
| 03 | `03_observability.zig` | wire `Config.on_event` to log every lifecycle transition | `zig build run-03-observability` |
-| 04 | `04_health_check.zig` | ping loop with cross-thread cancellation token | `zig build run-04-health-check` |
+| 04 | `04_health_check.zig` | ping loop in an `io.concurrent` task, stopped with `Future.cancel` | `zig build run-04-health-check` |
| 05 | `05_custom_transport.zig` | plug a `MemTransport` (canned bytes) into `Client.fromTransport` | `zig build run-05-custom-transport` |
+| 06 | `06_concurrent_queries.zig` | `Pool` + `Io.Group` fan-out, and a query deadline via `Future.cancel` | `zig build run-06-concurrent-queries` |
`zig build examples` compiles all of them as a smoke check.
+## benchmarks
+
+
+
+| workload | clickzig | clickhouse-client (C++) |
+|---|---:|---:|
+| UInt64 scan (20M rows) | 467.9M rows/s | 454.5M rows/s |
+| UInt64 scan + LZ4 | 129.8M rows/s | 114.9M rows/s |
+| String scan (5M rows) | 91.2M rows/s | 83.3M rows/s |
+| String scan + LZ4 | 57.1M rows/s | 62.5M rows/s |
+| INSERT 100k rows × 3 cols, Null engine | 1.7M rows/s | 1.6M rows/s |
+
+Concurrent queries over a `Pool` (workers → queries/s): 1→101 · 2→152 · 4→216 · 8→265 · 16→266. Ping round trip p50 19 µs, p99 40 µs.
+
+Zig 0.17.1 · ClickHouse 26.3 · AMD Ryzen 7 7840HS (16 threads) · Linux · Io.Threaded. Loopback TCP with client and server in the same container, so these numbers show client overhead, not network speed. `clickhouse-client` is a reference point rather than a strict head-to-head: it also re-encodes the Native output it receives. INSERT is server-bound, so both clients land at the same rate whatever the compression.
+
+Reproduce (ClickHouse on `127.0.0.1:9000`, `default:test`):
+
+```bash
+zig build bench -Doptimize=ReleaseFast > run1.jsonl # repeat for run2, run3
+sh bench/reference_clickhouse_client.sh > ref.jsonl # inside the ClickHouse container
+python3 bench/collect.py --env "" --reference ref.jsonl run*.jsonl > bench/results.json
+python3 bench/plot.py bench/results.json assets/benchmarks.svg
+```
+
## documentation
The root README is the short overview. The full docs live in `docs/`:
@@ -196,7 +246,7 @@ Connect errors are typed and granular. The OS-level cause flows into specific `C
| `ReadTimeout` | established socket read exceeded `read_timeout_ms` |
| `WriteTimeout` | established socket write exceeded `write_timeout_ms` |
| `ProtocolError` | malformed wire bytes (overlong varint, unexpected packet, length cap) |
-| `Cancelled` | caller's atomic bool flipped mid-handshake |
+| `Canceled` | the calling `std.Io` task was canceled (`Future.cancel` / `Group.cancel`) |
| `ConnectionFailed` | catch-all for unmapped OS errors |
## porting from other clients
@@ -242,6 +292,9 @@ zig build test
# end-to-end smoke against a running ClickHouse on localhost:9000
zig build smoke --
+
+# async + cancellation scenarios (cancel-query, stream-cancel, cancel-race, pool-cancel, fanout)
+zig build smoke-async -- all
```
Scenarios: `happy`, `ping`, `wrong-pass`, `unreachable`, `wrong-host`, `query-bytes`, `query-mixed`, `insert-roundtrip`, `nullable-roundtrip`, `complex-types`, `tuple-map`, `decimal-ip`, `column-coverage`, `pool`, `pool-loop`, `dsn`, `compression`, `insert-compression`, `insert-zstd`, `nested-compressed-insert`, `lowcardinality`, `lc-write`, `timeout`, `read-timeout`, `write-timeout`, `json`, `dynamic`, `parameters`, `geo`, `external-data`, `combined-features`.
@@ -250,7 +303,7 @@ The smoke harness assumes `clickhouse/clickhouse-server:26.3` running locally wi
### branch policy
-`main` targets the latest supported stable Zig line for this repository: currently Zig 0.16.0. The `0.16.0` branch is the maintained Zig 0.16 line. A future `dev` branch may track Zig development snapshots. Do not create GitHub Releases or semver tag trains for normal development.
+`main` targets the latest supported stable Zig line for this repository: currently Zig 0.17. The `0.16.0` branch is the maintained Zig 0.16 line. A future `dev` branch may track Zig development snapshots. Do not create GitHub Releases or semver tag trains for normal development.
## license
diff --git a/assets/benchmarks.svg b/assets/benchmarks.svg
new file mode 100644
index 0000000..70cba0b
--- /dev/null
+++ b/assets/benchmarks.svg
@@ -0,0 +1,19 @@
+clickzig benchmarks UInt64 scan: clickzig 468M rows/s vs clickhouse-client 455M rows/s; UInt64 scan + LZ4: clickzig 130M rows/s vs clickhouse-client 115M rows/s; String scan: clickzig 91M rows/s vs clickhouse-client 83M rows/s; String scan + LZ4: clickzig 57M rows/s vs clickhouse-client 62M rows/s clickzig benchmarks Zig 0.17.1 · ClickHouse 26.3 · AMD Ryzen 7 7840HS (16 threads) · Linux · Io.Threaded Read throughput — million rows/s (higher is better) clickhouse-client 26.3 (C++) clickzig 0 100 200 300 400 500 UInt64 scan 20M rows 468 455 UInt64 scan + LZ4 20M rows 130 115 String scan 5M rows 91 83 String scan + LZ4 5M rows 57 62 Concurrent queries over a Pool — queries/s 48 × SELECT 500k rows (UInt64 + String), Io.Group workers 0 100 200 300 101 1 152 2 216 4 265 8 266 16 workers (connections) Ping round trip (p50) 19 µs p99 40 µs · 20k sequential pings INSERT, 100k rows × 3 columns 1.65M rows/s clickhouse-client: 1.64M rows/s · server-bound Loopback TCP, client and server in the same container. Medians of 7 timed passes (3 runs for clickzig), ReleaseFast. clickhouse-client is a reference, not a strict head-to-head: it also re-encodes Native output (to /dev/null); time via --time. INSERT goes to an ENGINE = Null table; both clients spend ~60ms/batch waiting on the server, so compression does not change it.
diff --git a/bench/bench.zig b/bench/bench.zig
new file mode 100644
index 0000000..8834096
--- /dev/null
+++ b/bench/bench.zig
@@ -0,0 +1,250 @@
+//! clickzig benchmark harness.
+//!
+//! Talks to a real ClickHouse on 127.0.0.1:9000 (`default:test`) and
+//! prints one JSON object per measurement to stdout. Build with
+//! `zig build bench -Doptimize=ReleaseFast`; `bench/plot.py` turns the
+//! output into `assets/benchmarks.svg`.
+//!
+//! Every measurement runs one warm-up pass, then `runs` timed passes,
+//! and reports the median.
+
+const std = @import("std");
+const clickzig = @import("clickzig");
+
+const Io = std.Io;
+
+const runs = 7;
+
+pub fn main(init: std.process.Init) !void {
+ const gpa = init.gpa;
+ const io = init.io;
+
+ var out_buf: [4096]u8 = undefined;
+ var out_file = Io.File.stdout().writer(io, &out_buf);
+ const out = &out_file.interface;
+
+ try benchRead(gpa, io, out, "read_uint64", "SELECT number FROM numbers(20000000)", 20_000_000, 8, .Disable);
+ try benchRead(gpa, io, out, "read_uint64_lz4", "SELECT number FROM numbers(20000000)", 20_000_000, 8, .Enable);
+ try benchRead(gpa, io, out, "read_string", "SELECT toString(number) AS s FROM numbers(5000000)", 5_000_000, null, .Disable);
+ try benchRead(gpa, io, out, "read_string_lz4", "SELECT toString(number) AS s FROM numbers(5000000)", 5_000_000, null, .Enable);
+ try out.flush();
+
+ try benchInsert(gpa, io, out, "insert_none", .Disable, .lz4);
+ try benchInsert(gpa, io, out, "insert_lz4", .Enable, .lz4);
+ try benchInsert(gpa, io, out, "insert_zstd", .Enable, .zstd);
+ try out.flush();
+
+ try benchPing(gpa, io, out);
+ try out.flush();
+
+ for ([_]u32{ 1, 2, 4, 8, 16 }) |concurrency| {
+ try benchConcurrency(gpa, io, out, concurrency);
+ try out.flush();
+ }
+}
+
+fn config(gpa: std.mem.Allocator) clickzig.Config {
+ return .{
+ .control_allocator = gpa,
+ .read_buffer_size = 256 * 1024,
+ .write_buffer_size = 256 * 1024,
+ .username = "default",
+ .password = "test",
+ };
+}
+
+fn nowNs(io: Io) i96 {
+ return Io.Clock.now(.awake, io).nanoseconds;
+}
+
+fn median(samples: []u64) u64 {
+ std.mem.sort(u64, samples, {}, std.sort.asc(u64));
+ return samples[samples.len / 2];
+}
+
+// --- reads ----------------------------------------------------------------
+
+fn drain(client: *clickzig.Client, gpa: std.mem.Allocator, sql: []const u8, opts: clickzig.QueryOptions) !u64 {
+ var arena: std.heap.ArenaAllocator = .init(gpa);
+ defer arena.deinit();
+ var stream = try client.query(sql, arena.allocator(), opts);
+ var rows: u64 = 0;
+ while (try stream.next()) |packet| {
+ switch (packet) {
+ .data => |b| rows += b.num_rows,
+ .exception => |e| {
+ std.debug.print("server exception: {s}\n", .{e.message});
+ return error.ServerException;
+ },
+ .end_of_stream => break,
+ else => {},
+ }
+ // Blocks are independent; free them as we go so a long scan
+ // measures decoding, not arena growth.
+ _ = arena.reset(.retain_capacity);
+ }
+ return rows;
+}
+
+fn benchRead(
+ gpa: std.mem.Allocator,
+ io: Io,
+ out: *Io.Writer,
+ name: []const u8,
+ sql: []const u8,
+ expected_rows: u64,
+ bytes_per_row: ?u64,
+ compression: clickzig.protocol.CompressionEnabled,
+) !void {
+ const client = try clickzig.Client.connectTcp(config(gpa), io, null);
+ defer client.close();
+ const opts: clickzig.QueryOptions = .{ .compression = compression };
+
+ _ = try drain(client, gpa, sql, opts);
+ var samples: [runs]u64 = undefined;
+ for (&samples) |*sample| {
+ const t0 = nowNs(io);
+ const rows = try drain(client, gpa, sql, opts);
+ sample.* = @intCast(nowNs(io) - t0);
+ if (rows != expected_rows) return error.RowCountMismatch;
+ }
+ const ns = median(&samples);
+ const rows_per_s = @as(f64, @floatFromInt(expected_rows)) / (@as(f64, @floatFromInt(ns)) / 1e9);
+ try out.print("{{\"bench\":\"{s}\",\"rows\":{d},\"ns\":{d},\"rows_per_s\":{d:.0}", .{ name, expected_rows, ns, rows_per_s });
+ if (bytes_per_row) |b| try out.print(",\"mb_per_s\":{d:.1}", .{rows_per_s * @as(f64, @floatFromInt(b)) / 1e6});
+ try out.writeAll("}\n");
+}
+
+// --- inserts --------------------------------------------------------------
+
+const insert_batch_rows = 100_000;
+const insert_batches = 10;
+
+fn benchInsert(
+ gpa: std.mem.Allocator,
+ io: Io,
+ out: *Io.Writer,
+ name: []const u8,
+ compression: clickzig.protocol.CompressionEnabled,
+ method: clickzig.compression.WriteMethod,
+) !void {
+ const client = try clickzig.Client.connectTcp(config(gpa), io, null);
+ defer client.close();
+ _ = try drain(client, gpa, "DROP TABLE IF EXISTS bench_insert", .{});
+ _ = try drain(client, gpa, "CREATE TABLE bench_insert (id UInt64, label String, value Float64) ENGINE = Null", .{});
+
+ var arena: std.heap.ArenaAllocator = .init(gpa);
+ defer arena.deinit();
+ const a = arena.allocator();
+ const ids = try a.alloc(u64, insert_batch_rows);
+ const labels = try a.alloc([]u8, insert_batch_rows);
+ const values = try a.alloc(f64, insert_batch_rows);
+ for (ids, labels, values, 0..) |*id, *label, *value, i| {
+ id.* = i;
+ label.* = try a.print("event-{d}-{s}", .{ i % 5000, if (i % 3 == 0) "click" else "view" });
+ value.* = @as(f64, @floatFromInt(i)) * 0.25;
+ }
+ const cols = [_]clickzig.block.InsertColumn{
+ .{ .name = "id", .type_name = "UInt64", .data = .{ .UInt64 = ids } },
+ .{ .name = "label", .type_name = "String", .data = .{ .String = labels } },
+ .{ .name = "value", .type_name = "Float64", .data = .{ .Float64 = values } },
+ };
+ const opts: clickzig.QueryOptions = .{ .compression = compression, .compression_method = method };
+ const sql = "INSERT INTO bench_insert (id, label, value) FORMAT Native";
+
+ var scratch: std.heap.ArenaAllocator = .init(gpa);
+ defer scratch.deinit();
+ try client.insert(sql, "", insert_batch_rows, &cols, scratch.allocator(), opts);
+
+ var samples: [runs]u64 = undefined;
+ for (&samples) |*sample| {
+ const t0 = nowNs(io);
+ for (0..insert_batches) |_| {
+ _ = scratch.reset(.retain_capacity);
+ try client.insert(sql, "", insert_batch_rows, &cols, scratch.allocator(), opts);
+ }
+ sample.* = @intCast(nowNs(io) - t0);
+ }
+ const ns = median(&samples);
+ const total_rows: u64 = insert_batch_rows * insert_batches;
+ const rows_per_s = @as(f64, @floatFromInt(total_rows)) / (@as(f64, @floatFromInt(ns)) / 1e9);
+ try out.print("{{\"bench\":\"{s}\",\"rows\":{d},\"ns\":{d},\"rows_per_s\":{d:.0}}}\n", .{ name, total_rows, ns, rows_per_s });
+ _ = try drain(client, gpa, "DROP TABLE IF EXISTS bench_insert", .{});
+}
+
+// --- ping -----------------------------------------------------------------
+
+fn benchPing(gpa: std.mem.Allocator, io: Io, out: *Io.Writer) !void {
+ const client = try clickzig.Client.connectTcp(config(gpa), io, null);
+ defer client.close();
+ const n = 20_000;
+ const samples = try gpa.alloc(u64, n);
+ defer gpa.free(samples);
+ for (0..1000) |_| try client.ping();
+ for (samples) |*sample| {
+ const t0 = nowNs(io);
+ try client.ping();
+ sample.* = @intCast(nowNs(io) - t0);
+ }
+ std.mem.sort(u64, samples, {}, std.sort.asc(u64));
+ try out.print("{{\"bench\":\"ping\",\"n\":{d},\"p50_ns\":{d},\"p99_ns\":{d}}}\n", .{ n, samples[n / 2], samples[n * 99 / 100] });
+}
+
+// --- concurrency ----------------------------------------------------------
+
+const concurrent_queries = 48;
+const concurrent_sql = "SELECT number, toString(number) FROM numbers(500000)";
+
+const Worker = struct {
+ rows: u64 = 0,
+ err: ?anyerror = null,
+};
+
+/// One worker = one connection. Workers pull query indexes from a shared
+/// counter until all `concurrent_queries` are done.
+fn worker(pool: *clickzig.Pool, gpa: std.mem.Allocator, next: *std.atomic.Value(u32), w: *Worker) Io.Cancelable!void {
+ workerInner(pool, gpa, next, w) catch |e| switch (e) {
+ error.Canceled => return error.Canceled,
+ else => w.err = e,
+ };
+}
+
+fn workerInner(pool: *clickzig.Pool, gpa: std.mem.Allocator, next: *std.atomic.Value(u32), w: *Worker) !void {
+ const client = try pool.acquire();
+ defer pool.release(client);
+ while (next.fetchAdd(1, .monotonic) < concurrent_queries) {
+ w.rows += try drain(client, gpa, concurrent_sql, .{});
+ }
+}
+
+fn runConcurrentBatch(pool: *clickzig.Pool, gpa: std.mem.Allocator, io: Io, concurrency: u32) !void {
+ var workers: [16]Worker = @splat(.{});
+ var next: std.atomic.Value(u32) = .init(0);
+ var group: Io.Group = .init;
+ defer group.cancel(io);
+ // `concurrent`, not `async`: every worker must really be in flight.
+ for (workers[0..concurrency]) |*w| try group.concurrent(io, worker, .{ pool, gpa, &next, w });
+ try group.await(io);
+ var rows: u64 = 0;
+ for (workers[0..concurrency]) |w| {
+ if (w.err) |e| return e;
+ rows += w.rows;
+ }
+ if (rows != concurrent_queries * 500_000) return error.RowCountMismatch;
+}
+
+fn benchConcurrency(gpa: std.mem.Allocator, io: Io, out: *Io.Writer, concurrency: u32) !void {
+ const pool = try clickzig.Pool.init(gpa, io, config(gpa), .{ .max_size = concurrency });
+ defer pool.deinit();
+
+ try runConcurrentBatch(pool, gpa, io, concurrency);
+ var samples: [5]u64 = undefined;
+ for (&samples) |*sample| {
+ const t0 = nowNs(io);
+ try runConcurrentBatch(pool, gpa, io, concurrency);
+ sample.* = @intCast(nowNs(io) - t0);
+ }
+ const ns = median(&samples);
+ const qps = @as(f64, @floatFromInt(concurrent_queries)) / (@as(f64, @floatFromInt(ns)) / 1e9);
+ try out.print("{{\"bench\":\"concurrency\",\"concurrency\":{d},\"queries\":{d},\"ns\":{d},\"queries_per_s\":{d:.1},\"rows_per_s\":{d:.0}}}\n", .{ concurrency, concurrent_queries, ns, qps, qps * 500_000 });
+}
diff --git a/bench/collect.py b/bench/collect.py
new file mode 100644
index 0000000..ebc01c8
--- /dev/null
+++ b/bench/collect.py
@@ -0,0 +1,74 @@
+#!/usr/bin/env python3
+"""Merge raw benchmark output into bench/results.json.
+
+Usage:
+ python3 bench/collect.py --env "machine description" \
+ --reference ref.jsonl run1.jsonl [run2.jsonl ...] > bench/results.json
+
+Each clickzig run file holds the JSON lines printed by `zig build bench`.
+Across runs, every measurement keeps the median duration. The reference
+file holds the lines printed by bench/reference_clickhouse_client.sh.
+"""
+
+import argparse
+import json
+import statistics
+from collections import defaultdict
+
+
+def main():
+ ap = argparse.ArgumentParser()
+ ap.add_argument("--env", required=True)
+ ap.add_argument("--reference", required=True)
+ ap.add_argument("runs", nargs="+")
+ args = ap.parse_args()
+
+ durations = defaultdict(list)
+ rows = {}
+ conc = defaultdict(list)
+ ping_p50, ping_p99 = [], []
+ for path in args.runs:
+ for line in open(path):
+ r = json.loads(line)
+ if r["bench"] == "ping":
+ ping_p50.append(r["p50_ns"])
+ ping_p99.append(r["p99_ns"])
+ elif r["bench"] == "concurrency":
+ conc[r["concurrency"]].append(r["ns"])
+ rows["concurrency"] = r["queries"]
+ else:
+ durations[r["bench"]].append(r["ns"])
+ rows[r["bench"]] = r["rows"]
+
+ clickzig = {}
+ for name, ns_list in durations.items():
+ ns = statistics.median(ns_list)
+ clickzig[name] = {"rows": rows[name], "ns": ns, "rows_per_s": rows[name] / (ns / 1e9)}
+
+ concurrency = []
+ for c in sorted(conc):
+ ns = statistics.median(conc[c])
+ concurrency.append({"concurrency": c, "ns": ns, "queries_per_s": rows["concurrency"] / (ns / 1e9)})
+
+ reference = {}
+ for line in open(args.reference):
+ r = json.loads(line)
+ reference[r["bench"]] = {"rows": r["rows"], "s": r["s"], "rows_per_s": r["rows"] / r["s"]}
+
+ result = {
+ "environment": args.env,
+ "clickzig": clickzig,
+ "reference": reference,
+ "concurrency": concurrency,
+ "ping": {"p50_ns": statistics.median(ping_p50), "p99_ns": statistics.median(ping_p99)},
+ "notes": [
+ "Loopback TCP, client and server in the same container. Medians of 7 timed passes (3 runs for clickzig), ReleaseFast.",
+ "clickhouse-client is a reference, not a strict head-to-head: it also re-encodes Native output (to /dev/null); time via --time.",
+ "INSERT goes to an ENGINE = Null table; both clients spend ~60ms/batch waiting on the server, so compression does not change it.",
+ ],
+ }
+ print(json.dumps(result, indent=2))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/bench/plot.py b/bench/plot.py
new file mode 100644
index 0000000..3917bff
--- /dev/null
+++ b/bench/plot.py
@@ -0,0 +1,239 @@
+#!/usr/bin/env python3
+"""Render clickzig benchmark results to assets/benchmarks.svg.
+
+Usage:
+ python3 bench/plot.py RESULTS_JSON [OUT_SVG]
+
+RESULTS_JSON is produced by bench/collect.py (merged clickzig runs plus
+the clickhouse-client reference). No third-party dependencies: the SVG
+is written by hand so it stays small, crisp, and theme-aware (it follows
+the viewer's light/dark preference via prefers-color-scheme).
+"""
+
+import json
+import sys
+from html import escape
+
+W = 880
+PAD = 32
+
+LIGHT = {
+ "surface": "#fcfcfb",
+ "text1": "#0b0b0b",
+ "text2": "#52514e",
+ "text3": "#6f6e69",
+ "grid": "#e4e3df",
+ "axis": "#c9c8c2",
+ "s1": "#2a78d6",
+ "s2": "#eb6834",
+ "tile": "#f4f3f0",
+}
+DARK = {
+ "surface": "#1a1a19",
+ "text1": "#f0efec",
+ "text2": "#c3c2b7",
+ "text3": "#9a998f",
+ "grid": "#2c2c2a",
+ "axis": "#45453f",
+ "s1": "#3987e5",
+ "s2": "#d95926",
+ "tile": "#242423",
+}
+
+READ_ROWS = [
+ ("read_uint64", "UInt64 scan", "20M rows"),
+ ("read_uint64_lz4", "UInt64 scan + LZ4", "20M rows"),
+ ("read_string", "String scan", "5M rows"),
+ ("read_string_lz4", "String scan + LZ4", "5M rows"),
+]
+
+
+def css():
+ def block(p):
+ return "".join(
+ f"--{k}:{v};" for k, v in p.items()
+ )
+
+ return f"""
+svg {{ {block(LIGHT)} font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; }}
+@media (prefers-color-scheme: dark) {{ svg {{ {block(DARK)} }} }}
+.bg {{ fill: var(--surface); }}
+.t1 {{ fill: var(--text1); }}
+.t2 {{ fill: var(--text2); }}
+.t3 {{ fill: var(--text3); }}
+.grid {{ stroke: var(--grid); stroke-width: 1; }}
+.axis {{ stroke: var(--axis); stroke-width: 1; }}
+.s1 {{ fill: var(--s1); }}
+.s2 {{ fill: var(--s2); }}
+.tile {{ fill: var(--tile); }}
+.h1 {{ font-size: 20px; font-weight: 650; }}
+.h2 {{ font-size: 14px; font-weight: 600; }}
+.sm {{ font-size: 12px; }}
+.xs {{ font-size: 11px; }}
+.num {{ font-variant-numeric: tabular-nums; }}
+.big {{ font-size: 26px; font-weight: 650; }}
+"""
+
+
+def hbar(x, y, w, h, cls, r=4):
+ """Horizontal bar anchored at x, rounded only at the data end."""
+ w = max(w, r + 1)
+ return (
+ f' '
+ )
+
+
+def vbar(x, base, w, h, cls, r=4):
+ """Vertical bar anchored at the baseline, rounded only at the top."""
+ h = max(h, r + 1)
+ y = base - h
+ return (
+ f' '
+ )
+
+
+def text(x, y, s, cls, anchor="start"):
+ return f'{escape(s)} '
+
+
+def nice_max(v, step):
+ return step * (int(v / step) + 1)
+
+
+def render(res):
+ out = []
+ y = PAD
+
+ out.append(text(PAD, y + 18, "clickzig benchmarks", "t1 h1"))
+ out.append(text(PAD, y + 40, res["environment"], "t2 sm"))
+ y += 64
+
+ # ---- Panel A: read throughput --------------------------------------
+ out.append(text(PAD, y + 14, "Read throughput — million rows/s (higher is better)", "t1 h2"))
+ # legend
+ lx = W - PAD
+ items = [("clickhouse-client 26.3 (C++)", "s2"), ("clickzig", "s1")]
+ for label, cls in items:
+ tw = 7.0 * len(label)
+ lx -= tw
+ out.append(text(lx, y + 14, label, "t2 sm"))
+ lx -= 16
+ out.append(f' ')
+ lx -= 18
+ y += 30
+
+ label_w = 170
+ plot_x = PAD + label_w
+ plot_w = W - PAD - plot_x - 56
+ vmax = max(
+ max(res["clickzig"][k]["rows_per_s"], res["reference"].get(k, {}).get("rows_per_s", 0))
+ for k, _, _ in READ_ROWS
+ ) / 1e6
+ xmax = nice_max(vmax, 100)
+ bar_h = 14
+ group_h = bar_h * 2 + 2 + 18
+ plot_h = group_h * len(READ_ROWS)
+
+ for tick in range(0, xmax + 1, 100):
+ gx = plot_x + plot_w * tick / xmax
+ out.append(f' ')
+ out.append(text(gx, y + plot_h + 4, f"{tick}", "t3 xs num", "middle"))
+
+ for i, (key, name, sub) in enumerate(READ_ROWS):
+ gy = y + i * group_h
+ out.append(text(PAD, gy + 13, name, "t1 sm"))
+ out.append(text(PAD, gy + 28, sub, "t3 xs"))
+ cz = res["clickzig"][key]["rows_per_s"] / 1e6
+ ref = res["reference"].get(key, {}).get("rows_per_s", 0) / 1e6
+ for j, (v, cls) in enumerate([(cz, "s1"), (ref, "s2")]):
+ by = gy + j * (bar_h + 2)
+ bw = plot_w * v / xmax
+ out.append(hbar(plot_x, by, bw, bar_h, cls))
+ out.append(text(plot_x + bw + 6, by + bar_h - 3, f"{v:,.0f}", "t2 xs num"))
+ out.append(f' ')
+ y += plot_h + 26
+
+ # ---- Panel B: concurrency + Panel C: tiles --------------------------
+ col_w = (W - 2 * PAD - 32) / 2
+ bx = PAD
+ out.append(text(bx, y + 14, "Concurrent queries over a Pool — queries/s", "t1 h2"))
+ out.append(text(bx, y + 32, "48 × SELECT 500k rows (UInt64 + String), Io.Group workers", "t3 xs"))
+ cy = y + 48
+ conc = res["concurrency"]
+ cmax = nice_max(max(c["queries_per_s"] for c in conc), 100)
+ ch = 150
+ base = cy + ch
+ for tick in range(0, cmax + 1, 100):
+ gy = base - ch * tick / cmax
+ out.append(f' ')
+ out.append(text(bx + 22, gy + 4, f"{tick}", "t3 xs num", "end"))
+ n = len(conc)
+ slot = (col_w - 36) / n
+ bw = min(44, slot - 10)
+ for i, c in enumerate(conc):
+ x0 = bx + 36 + i * slot + (slot - bw) / 2
+ h = ch * c["queries_per_s"] / cmax
+ out.append(vbar(x0, base, bw, h, "s1"))
+ out.append(text(x0 + bw / 2, base - h - 6, f"{c['queries_per_s']:.0f}", "t2 xs num", "middle"))
+ out.append(text(x0 + bw / 2, base + 16, f"{c['concurrency']}", "t2 xs num", "middle"))
+ out.append(f' ')
+ out.append(text(bx + 36 + (col_w - 36) / 2, base + 32, "workers (connections)", "t3 xs", "middle"))
+
+ tx = PAD + col_w + 32
+ tw = col_w
+ th = 92
+ tiles = [
+ (
+ "Ping round trip (p50)",
+ f"{res['ping']['p50_ns'] / 1000:.0f} µs",
+ f"p99 {res['ping']['p99_ns'] / 1000:.0f} µs · 20k sequential pings",
+ ),
+ (
+ "INSERT, 100k rows × 3 columns",
+ f"{res['clickzig']['insert_none']['rows_per_s'] / 1e6:.2f}M rows/s",
+ f"clickhouse-client: {res['reference']['insert_none']['rows_per_s'] / 1e6:.2f}M rows/s · server-bound",
+ ),
+ ]
+ for i, (title, big, sub) in enumerate(tiles):
+ ty = y + 8 + i * (th + 14)
+ out.append(f' ')
+ out.append(text(tx + 16, ty + 24, title, "t2 sm"))
+ out.append(text(tx + 16, ty + 58, big, "t1 big num"))
+ out.append(text(tx + 16, ty + 80, sub, "t3 xs"))
+ y = max(base + 40, y + 8 + 2 * th + 14) + 18
+
+ for line in res["notes"]:
+ out.append(text(PAD, y, line, "t3 xs"))
+ y += 16
+ y += PAD - 12
+
+ title = "clickzig benchmarks"
+ desc = "; ".join(
+ f"{name}: clickzig {res['clickzig'][k]['rows_per_s'] / 1e6:.0f}M rows/s vs clickhouse-client "
+ f"{res['reference'].get(k, {}).get('rows_per_s', 0) / 1e6:.0f}M rows/s"
+ for k, name, _ in READ_ROWS
+ )
+ head = (
+ f''
+ f'{escape(title)} {escape(desc)} '
+ f""
+ f' '
+ )
+ return head + "".join(out) + " \n"
+
+
+def main():
+ res = json.load(open(sys.argv[1]))
+ out = sys.argv[2] if len(sys.argv) > 2 else "assets/benchmarks.svg"
+ with open(out, "w") as f:
+ f.write(render(res))
+ print(f"wrote {out}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/bench/reference_clickhouse_client.sh b/bench/reference_clickhouse_client.sh
new file mode 100755
index 0000000..902522d
--- /dev/null
+++ b/bench/reference_clickhouse_client.sh
@@ -0,0 +1,20 @@
+#!/bin/sh
+# Reference timings with clickhouse-client (C++), run inside the ClickHouse
+# container. Prints JSON lines for bench/collect.py --reference.
+CH="clickhouse-client --password test"
+median() { sort -n | awk '{a[NR]=$1} END {print a[int((NR+1)/2)]}'; }
+run() { # name compression rows query
+ name=$1; comp=$2; rows=$3; q=$4
+ $CH --compression $comp -q "$q FORMAT Native" >/dev/null 2>&1
+ t=$(for i in 1 2 3 4 5 6 7; do $CH --compression $comp --time -q "$q FORMAT Native" 2>&1 >/dev/null | tail -1; done | median)
+ echo "{\"bench\":\"$name\",\"client\":\"clickhouse-client\",\"rows\":$rows,\"s\":$t}"
+}
+run read_uint64 0 20000000 "SELECT number FROM numbers(20000000)"
+run read_uint64_lz4 1 20000000 "SELECT number FROM numbers(20000000)"
+run read_string 0 5000000 "SELECT toString(number) AS s FROM numbers(5000000)"
+run read_string_lz4 1 5000000 "SELECT toString(number) AS s FROM numbers(5000000)"
+$CH -q "CREATE TABLE IF NOT EXISTS bench_insert (id UInt64, label String, value Float64) ENGINE = Null"
+$CH -q "SELECT number AS id, concat('event-', toString(number % 5000), '-', if(number % 3 = 0, 'click', 'view')) AS label, number*0.25 AS value FROM numbers(100000) FORMAT Native" > /tmp/x.native
+t=$(for i in 1 2 3 4 5 6 7; do $CH --compression 0 --time -q "INSERT INTO bench_insert FORMAT Native" < /tmp/x.native 2>&1 | tail -1; done | median)
+echo "{\"bench\":\"insert_none\",\"client\":\"clickhouse-client\",\"rows\":100000,\"s\":$t}"
+$CH -q "DROP TABLE bench_insert"
diff --git a/bench/results.json b/bench/results.json
new file mode 100644
index 0000000..c62652b
--- /dev/null
+++ b/bench/results.json
@@ -0,0 +1,103 @@
+{
+ "environment": "Zig 0.17.1 \u00b7 ClickHouse 26.3 \u00b7 AMD Ryzen 7 7840HS (16 threads) \u00b7 Linux \u00b7 Io.Threaded",
+ "clickzig": {
+ "read_uint64": {
+ "rows": 20000000,
+ "ns": 42748041,
+ "rows_per_s": 467857696.6836913
+ },
+ "read_uint64_lz4": {
+ "rows": 20000000,
+ "ns": 154036358,
+ "rows_per_s": 129839475.95021689
+ },
+ "read_string": {
+ "rows": 5000000,
+ "ns": 54835566,
+ "rows_per_s": 91181697.65950806
+ },
+ "read_string_lz4": {
+ "rows": 5000000,
+ "ns": 87556005,
+ "rows_per_s": 57106305.84389956
+ },
+ "insert_none": {
+ "rows": 1000000,
+ "ns": 604290201,
+ "rows_per_s": 1654834.048847997
+ },
+ "insert_lz4": {
+ "rows": 1000000,
+ "ns": 604244335,
+ "rows_per_s": 1654959.6613098574
+ },
+ "insert_zstd": {
+ "rows": 1000000,
+ "ns": 604125102,
+ "rows_per_s": 1655286.2920104254
+ }
+ },
+ "reference": {
+ "read_uint64": {
+ "rows": 20000000,
+ "s": 0.044,
+ "rows_per_s": 454545454.54545456
+ },
+ "read_uint64_lz4": {
+ "rows": 20000000,
+ "s": 0.174,
+ "rows_per_s": 114942528.7356322
+ },
+ "read_string": {
+ "rows": 5000000,
+ "s": 0.06,
+ "rows_per_s": 83333333.33333334
+ },
+ "read_string_lz4": {
+ "rows": 5000000,
+ "s": 0.08,
+ "rows_per_s": 62500000.0
+ },
+ "insert_none": {
+ "rows": 100000,
+ "s": 0.061,
+ "rows_per_s": 1639344.262295082
+ }
+ },
+ "concurrency": [
+ {
+ "concurrency": 1,
+ "ns": 473184987,
+ "queries_per_s": 101.44024286214305
+ },
+ {
+ "concurrency": 2,
+ "ns": 315012409,
+ "queries_per_s": 152.37494977539123
+ },
+ {
+ "concurrency": 4,
+ "ns": 222437415,
+ "queries_per_s": 215.7910349749389
+ },
+ {
+ "concurrency": 8,
+ "ns": 181191455,
+ "queries_per_s": 264.9131549829433
+ },
+ {
+ "concurrency": 16,
+ "ns": 180419069,
+ "queries_per_s": 266.04726576878636
+ }
+ ],
+ "ping": {
+ "p50_ns": 18635,
+ "p99_ns": 39524
+ },
+ "notes": [
+ "Loopback TCP, client and server in the same container. Medians of 7 timed passes (3 runs for clickzig), ReleaseFast.",
+ "clickhouse-client is a reference, not a strict head-to-head: it also re-encodes Native output (to /dev/null); time via --time.",
+ "INSERT goes to an ENGINE = Null table; both clients spend ~60ms/batch waiting on the server, so compression does not change it."
+ ]
+}
diff --git a/build.zig b/build.zig
index 102170b..98cba80 100644
--- a/build.zig
+++ b/build.zig
@@ -54,10 +54,25 @@ pub fn build(b: *std.Build) void {
.root_module = smoke_module,
});
const run_smoke = b.addRunArtifact(smoke);
- if (b.args) |args| run_smoke.addArgs(args);
+ run_smoke.addPassthruArgs();
const smoke_step = b.step("smoke", "Run smoke scenarios. Pass scenario via -- e.g. zig build smoke -- happy");
smoke_step.dependOn(&run_smoke.step);
+ const smoke_async_module = b.createModule(.{
+ .root_source_file = b.path("tests/smoke_async.zig"),
+ .target = target,
+ .optimize = optimize,
+ });
+ smoke_async_module.addImport("clickzig", clickzig_module);
+ const smoke_async = b.addExecutable(.{
+ .name = "smoke_async",
+ .root_module = smoke_async_module,
+ });
+ const run_smoke_async = b.addRunArtifact(smoke_async);
+ run_smoke_async.addPassthruArgs();
+ const smoke_async_step = b.step("smoke-async", "Run async/cancellation smoke scenarios (default: all)");
+ smoke_async_step.dependOn(&run_smoke_async.step);
+
const smoke_compression_step = b.step("smoke-compression", "Run ClickHouse-backed compression smoke scenarios");
const run_smoke_select_compression = b.addRunArtifact(smoke);
run_smoke_select_compression.addArg("compression");
@@ -71,10 +86,10 @@ pub fn build(b: *std.Build) void {
const smoke_readiness_step = b.step("smoke-readiness", "Run ClickHouse-backed branch-readiness smoke scenarios");
const readiness_scenarios = [_][]const u8{
- "pool", "pool-loop", "timeout", "read-timeout", "write-timeout",
- "compression", "insert-compression", "insert-zstd", "nested-compressed-insert",
- "lowcardinality", "lc-write", "json", "dynamic", "parameters",
- "geo", "external-data", "combined-features", "column-coverage",
+ "pool", "pool-loop", "timeout", "read-timeout", "write-timeout",
+ "compression", "insert-compression", "insert-zstd", "nested-compressed-insert", "lowcardinality",
+ "lc-write", "json", "dynamic", "parameters", "geo",
+ "external-data", "combined-features", "column-coverage",
};
for (readiness_scenarios) |scenario| {
const run_readiness_smoke = b.addRunArtifact(smoke);
@@ -82,6 +97,23 @@ pub fn build(b: *std.Build) void {
smoke_readiness_step.dependOn(&run_readiness_smoke.step);
}
+ // Benchmarks against a live ClickHouse. Build with
+ // `-Doptimize=ReleaseFast`; see bench/plot.py for the SVG.
+ const bench_module = b.createModule(.{
+ .root_source_file = b.path("bench/bench.zig"),
+ .target = target,
+ .optimize = optimize,
+ });
+ bench_module.addImport("clickzig", clickzig_module);
+ const bench_exe = b.addExecutable(.{
+ .name = "bench",
+ .root_module = bench_module,
+ });
+ const run_bench = b.addRunArtifact(bench_exe);
+ run_bench.addPassthruArgs();
+ const bench_step = b.step("bench", "Run benchmarks against ClickHouse on 127.0.0.1:9000 (use -Doptimize=ReleaseFast)");
+ bench_step.dependOn(&run_bench.step);
+
// --- Examples ---
// Each example is its own runnable: `zig build run-01-connect`,
// `zig build run-02-diagnostics`, etc. `zig build examples` builds
@@ -93,6 +125,7 @@ pub fn build(b: *std.Build) void {
.{ .name = "03-observability", .path = "examples/03_observability.zig" },
.{ .name = "04-health-check", .path = "examples/04_health_check.zig" },
.{ .name = "05-custom-transport", .path = "examples/05_custom_transport.zig" },
+ .{ .name = "06-concurrent-queries", .path = "examples/06_concurrent_queries.zig" },
};
for (example_files) |ex| {
const ex_module = b.createModule(.{
diff --git a/build.zig.zon b/build.zig.zon
index bc305c1..2184298 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -1,13 +1,14 @@
.{
.name = .clickzig,
- .version = "0.16.0",
+ .version = "0.17.0",
.fingerprint = 0x5491b43e7a7baa19,
- .minimum_zig_version = "0.16.0",
+ .minimum_zig_version = "0.17.0",
.dependencies = .{},
.paths = .{
"src",
"tests",
"examples",
+ "bench",
"build.zig",
"build.zig.zon",
".zigversion",
diff --git a/docs/README.md b/docs/README.md
index bc3479b..312ccf3 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1,20 +1,20 @@
# clickzig documentation
-This directory contains user-facing documentation for clickzig on Zig 0.16.0.
+This directory contains user-facing documentation for clickzig on Zig 0.17.
The README at the repository root is the quick overview; these pages are the
working reference.
## Guides
- [Getting started](getting-started.md): install, connect, query, insert, and run smoke tests.
-- [API guide](api.md): public types and common call patterns.
+- [API guide](api.md): public types, common call patterns, async usage, and cancellation.
- [Type mapping](types.md): ClickHouse type names and the corresponding `clickzig.Column` shapes.
- [Operations](operations.md): ClickHouse service setup, smoke scenarios, CI gates, pooling, TLS, and branch policy.
- [Security](security.md): defensive limits, timeout behavior, hostile-input probes, and supported wire assumptions.
## Version line
-`main` and `0.16.0` target Zig 0.16.0. The package version is intentionally
+`main` targets Zig 0.17; the `0.16.0` branch keeps the Zig 0.16 line. The package version is intentionally
aligned with the Zig compiler line rather than a separate semver release train.
No tag or release should be created unless the maintainer explicitly decides to
publish one.
diff --git a/docs/api.md b/docs/api.md
index 19f3ec7..22a396b 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -54,6 +54,8 @@ Important fields:
| `dial_timeout_ms` | `30000` | TCP connect timeout; `0` means infinite |
| `read_timeout_ms` | `60000` | socket read timeout; `0` means infinite |
| `write_timeout_ms` | `30000` | socket write timeout; `0` means infinite |
+| `tcp_no_delay` | `true` | set `TCP_NODELAY` on the built-in TCP transport |
+| `cancel_drain_timeout_ms` | `5000` | read budget for draining the server after a canceled query sends `Cancel` |
| `settings` | null | per-client default ClickHouse settings |
| `compression` | `.Disable` | opt-in wire compression |
| `compression_method` | `.lz4` | write method when compression is enabled |
@@ -66,23 +68,20 @@ allocated from the allocator passed to `query` or `insert`, not from
## Client lifecycle
```zig
-const client = try clickzig.Client.connectTcp(cfg, io, cancel, diag);
+const client = try clickzig.Client.connectTcp(cfg, io, diag);
defer client.close();
```
-`cancel` is an optional `*const std.atomic.Value(bool)`. If it flips to `true`
-before or during a supported I/O boundary, the operation returns
-`error.Cancelled`.
-
`diag` is an optional `*clickzig.Diagnostics`. On handshake exceptions it can
hold a parsed `ServerError`.
-One `Client` is not thread-safe. Use one client per thread or `clickzig.Pool`.
+One `Client` serves one in-flight call at a time and is not thread-safe. Give
+each concurrent task its own client, or share a `clickzig.Pool`.
## Query
```zig
-var stream = try client.query(sql, query_allocator, cancel, .{});
+var stream = try client.query(sql, query_allocator, .{});
while (try stream.next()) |packet| switch (packet) {
.data => |block| { /* read columns */ },
@@ -157,7 +156,7 @@ defer settings.deinit(allocator);
try settings.put(allocator, "max_threads", "4");
try settings.put(allocator, "readonly", "1");
-var stream = try client.query(sql, allocator, null, .{
+var stream = try client.query(sql, allocator, .{
.settings = &settings,
});
```
@@ -181,7 +180,6 @@ const external = [_]clickzig.ExternalTable{
var stream = try client.query(
"SELECT count() FROM ids",
allocator,
- null,
.{ .external_tables = &external },
);
```
@@ -213,6 +211,50 @@ The `type_name` string must match the server schema. Aliases such as `Date`,
`DateTime64(3)`, `Enum8(...)`, `LowCardinality(String)`, and `Nullable(UInt64)`
must be passed as the original ClickHouse type string.
+## Async and cancellation
+
+Every blocking step goes through the `std.Io` passed to `connectTcp` /
+`Pool.init`. Run calls concurrently with the usual `std.Io` tools:
+
+```zig
+// Concurrent fan-out over a pool.
+var group: std.Io.Group = .init;
+defer group.cancel(io);
+for (&jobs) |*job| group.async(io, runJob, .{ pool, gpa, job });
+try group.await(io);
+
+// A task that must make progress while the caller keeps going.
+var task = try io.concurrent(runQuery, .{ client, gpa });
+const result = task.await(io); // or task.cancel(io)
+```
+
+`io.async` may run the task inline to completion when no worker is free; use
+`io.concurrent` when the caller depends on the task running in parallel (for
+example, a deadline that sleeps and then cancels).
+
+Cancellation is `std.Io` cancellation. `future.cancel(io)` or
+`group.cancel(io)` makes the pending operation return `error.Canceled`, which
+every clickzig call propagates unchanged:
+
+| canceled while... | result |
+|---|---|
+| dialing / handshaking | `error.Canceled`; no client is returned |
+| before a query or ping wrote anything | `error.Canceled`; client stays `.ready` |
+| waiting for the next server packet | client sends protocol `Cancel`, drains to EndOfStream/Exception (bounded by `Config.cancel_drain_timeout_ms`, shielded from further cancels), returns `error.Canceled` with the client `.ready` |
+| in the middle of reading or writing a packet | `error.Canceled`; client is `.broken` |
+| waiting in `Pool.acquire` | `error.Canceled`; no pool slot is consumed |
+
+Check `client.isReusable()` after a cancel; `Pool.release` does this for you.
+
+To stop reading a result early without canceling the task, call
+`stream.cancel()`. It sends `Cancel`, drains, and returns the client to
+`.ready` (or `.broken` if the server does not wind down in time).
+
+Backend support (Zig 0.17.1): `std.Io.Threaded` implements networking,
+`async`/`concurrent`, and cancellation. `std.Io.Evented` (io_uring / kqueue)
+does not implement TCP connect or socket reads/writes yet, so it cannot drive
+clickzig until std fills that in.
+
## Pool
```zig
@@ -222,13 +264,15 @@ const pool = try clickzig.Pool.init(allocator, io, cfg, .{
});
defer pool.deinit();
-const client = try pool.acquire(null);
+const client = try pool.acquire();
defer pool.release(client);
-try client.ping(null);
+try client.ping();
```
The pool recycles clients only when `client.isReusable()` is true. Broken or
-expired clients are closed instead of returned to the idle list.
+expired clients are closed instead of returned to the idle list. The pool's
+mutex and condition are `std.Io` primitives, so it can be shared between
+threads or `std.Io` tasks, and `acquire()` is a cancellation point.
## TLS
@@ -247,7 +291,7 @@ const result = try clickzig.fromUri(
arena.allocator(),
defaults,
);
-const client = try clickzig.Client.connectTcp(result.config, io, null, null);
+const client = try clickzig.Client.connectTcp(result.config, io, null);
```
Unrecognized query parameters become ClickHouse settings. `client_name` is
diff --git a/docs/getting-started.md b/docs/getting-started.md
index 799167e..b1ebd98 100644
--- a/docs/getting-started.md
+++ b/docs/getting-started.md
@@ -1,11 +1,11 @@
# Getting started
-clickzig is a native-protocol ClickHouse client for Zig 0.16.0. It talks to the
+clickzig is a native-protocol ClickHouse client for Zig 0.17. It talks to the
ClickHouse TCP port directly: `9000` for plain TCP and `9440` for TLS.
## Requirements
-- Zig 0.16.0
+- Zig 0.17 (use the `0.16.0` branch for Zig 0.16)
- A ClickHouse server for smoke tests
- Native protocol credentials; the repository smoke tests assume user
`default`, password `test`, host `127.0.0.1`, and port `9000`
@@ -15,7 +15,7 @@ ClickHouse TCP port directly: `9000` for plain TCP and `9440` for TLS.
During development, depend on the branch that matches your Zig compiler:
```bash
-zig fetch --save git+https://github.com/JagritGumber/clickzig#0.16.0
+zig fetch --save git+https://github.com/JagritGumber/clickzig#main
```
Then import the module from your `build.zig`:
@@ -43,7 +43,7 @@ pub fn main(init: std.process.Init) !void {
.port = 9000,
.username = "default",
.password = "test",
- }, init.io, null, null);
+ }, init.io, null);
defer client.close();
var arena: std.heap.ArenaAllocator = .init(init.gpa);
@@ -52,7 +52,6 @@ pub fn main(init: std.process.Init) !void {
var stream = try client.query(
"SELECT number, number * 2 FROM numbers(5)",
arena.allocator(),
- null,
.{},
);
@@ -95,7 +94,6 @@ try client.insert(
ids.len,
&columns,
arena.allocator(),
- null,
.{},
);
```
@@ -115,7 +113,6 @@ try params.putString(arena.allocator(), "label", "clickzig");
var stream = try client.query(
"SELECT {n:UInt64} + 1, {label:String}",
arena.allocator(),
- null,
.{ .parameters = ¶ms },
);
```
@@ -141,7 +138,7 @@ var cfg: clickzig.Config = .{
You can also enable compression per query or insert:
```zig
-try client.insert(sql, "", rows, &columns, arena.allocator(), null, .{
+try client.insert(sql, "", rows, &columns, arena.allocator(), .{
.compression = .Enable,
.compression_method = .lz4,
});
diff --git a/docs/operations.md b/docs/operations.md
index c684eba..d9d911d 100644
--- a/docs/operations.md
+++ b/docs/operations.md
@@ -38,6 +38,7 @@ Run these with ClickHouse:
```bash
zig build smoke-compression
zig build smoke-readiness
+zig build smoke-async -- all
```
Focused smoke scenario:
@@ -82,6 +83,15 @@ Current scenarios:
- `external-data`
- `combined-features`
+Async scenarios (`zig build smoke-async -- `, default `all`):
+
+- `cancel-query` — cancel a long query from another task; the server must stop
+ it and the client must stay reusable
+- `stream-cancel` — `ResultStream.cancel()` after the first block
+- `cancel-race` — cancels at varying delays; every outcome must be clean
+- `pool-cancel` — cancel a task blocked in `Pool.acquire`
+- `fanout` — many concurrent queries through a `Pool` with `Io.Group`
+
`smoke-readiness` runs the release-confidence subset that exercises pooling,
timeouts, compression, custom types, external data, parameters, and combined
feature interactions.
@@ -96,6 +106,7 @@ zig build audit --summary all
zig build examples --summary all
zig build smoke-compression
zig build smoke-readiness
+zig build smoke-async -- all
```
CI should run the ClickHouse-backed steps against a service container exposing
@@ -103,8 +114,9 @@ native port `9000` with user `default` and password `test`.
## Pooling
-`Pool` is intended for multi-threaded applications that need more than one
-in-flight operation. A single `Client` is not thread-safe.
+`Pool` is intended for applications with more than one in-flight operation,
+whether they come from OS threads or `std.Io` tasks (`io.async`,
+`io.concurrent`, `Io.Group`). A single `Client` serves one call at a time.
```zig
const pool = try clickzig.Pool.init(allocator, io, cfg, .{
@@ -113,13 +125,14 @@ const pool = try clickzig.Pool.init(allocator, io, cfg, .{
});
defer pool.deinit();
-const client = try pool.acquire(null);
+const client = try pool.acquire();
defer pool.release(client);
```
Pool behavior:
-- `acquire(null)` waits for or creates a reusable connection.
+- `acquire()` waits for or creates a reusable connection; canceling the
+ waiting task returns `error.Canceled` without consuming a slot.
- `release(client)` recycles ready clients and closes broken clients.
- expired clients are retired according to `max_lifetime_ms`.
- callers must not use a client after releasing it.
@@ -140,8 +153,15 @@ Timeouts are configured on `Config` in milliseconds:
- `error.ReadTimeout`
- `error.WriteTimeout`
-The built-in `TcpTransport` implements timeout-aware dial/read/write behavior
-without relying on Zig 0.16's panicking TCP connect timeout path.
+The built-in `TcpTransport` enforces read/write timeouts with `std.Io.Batch`
+operations (`net_read` / `net_write` awaited with a timeout, then canceled), so
+no helper tasks are spawned per I/O call. Dial timeouts race the connect
+against a timer because Zig 0.17.1's threaded backend still panics when
+`connect` is given a timeout directly.
+
+A canceled query is aborted in-band: the client sends a protocol `Cancel` and
+drains the reply within `cancel_drain_timeout_ms` (default 5 s). Tune it if
+your server takes longer to stop heavy queries.
## TLS
@@ -159,7 +179,7 @@ local development and self-signed smoke setups only.
## Branch policy
`main` targets the current stable Zig line for this package, currently Zig
-0.16.0. The `0.16.0` branch is the maintained Zig 0.16 line. Future Zig
+0.17. The `0.16.0` branch is the maintained Zig 0.16 line. Future Zig
development snapshots can use a `dev` branch when needed.
Do not create release tags, GitHub Releases, or publish artifacts unless the
diff --git a/examples/01_connect.zig b/examples/01_connect.zig
index 0d609cf..41ed1fb 100644
--- a/examples/01_connect.zig
+++ b/examples/01_connect.zig
@@ -27,7 +27,7 @@ pub fn main(init: std.process.Init) !void {
.write_buffer_size = 4 * 1024,
.username = "default",
.password = "test",
- }, io, null, null);
+ }, io, null);
defer client.close();
const info = client.server_info;
diff --git a/examples/02_diagnostics.zig b/examples/02_diagnostics.zig
index 639ccb4..bd2969c 100644
--- a/examples/02_diagnostics.zig
+++ b/examples/02_diagnostics.zig
@@ -29,7 +29,7 @@ pub fn main(init: std.process.Init) !void {
.write_buffer_size = 4 * 1024,
.username = "default",
.password = "definitely-not-the-password",
- }, io, null, &diag);
+ }, io, &diag);
if (result) |client| {
client.close();
diff --git a/examples/03_observability.zig b/examples/03_observability.zig
index f905ef0..063f2c6 100644
--- a/examples/03_observability.zig
+++ b/examples/03_observability.zig
@@ -40,11 +40,11 @@ pub fn main(init: std.process.Init) !void {
.password = "test",
.on_event = EventLog.observe,
.on_event_ctx = &log,
- }, io, null, null);
+ }, io, null);
defer client.close();
- try client.ping(null);
- try client.ping(null);
+ try client.ping();
+ try client.ping();
std.debug.print("\nfinal counts:\n", .{});
var it = log.counts.iterator();
diff --git a/examples/04_health_check.zig b/examples/04_health_check.zig
index 233b60d..e65b1a0 100644
--- a/examples/04_health_check.zig
+++ b/examples/04_health_check.zig
@@ -1,68 +1,59 @@
-//! Example 04 — Health check loop with cancellation token.
+//! Example 04 — Health check loop with `std.Io` cancellation.
//!
-//! Pings the server in a loop, sleeping 100ms between rounds. A
-//! background "supervisor" thread flips an atomic bool after 1.5s,
-//! demonstrating the cancellation contract: the cancel token is polled
-//! at every I/O boundary inside `ping`, so the loop exits within at
-//! most one in-flight operation.
+//! Runs a ping loop as an `Io` task (`io.concurrent`), then cancels it
+//! from `main` after 1.5s. Cancellation is delivered by the `Io`
+//! runtime: whichever operation the task is blocked in (the socket read
+//! waiting for Pong, or the 100ms sleep) returns `error.Canceled`, and
+//! clickzig propagates it unchanged.
//!
//! Concepts:
-//! - `?*const std.atomic.Value(bool)` is the v0.16.0
-//! cancellation primitive (a future major may switch to Io.Cancel).
-//! - Polling cadence is documented per-operation in client.zig.
-//! - `client.isReusable()` lets a future Pool decide whether to
-//! recycle a connection or discard it after an error.
+//! - No cancel token: `future.cancel(io)` is the cancellation primitive.
+//! - The same code runs on `Io.Threaded` or an evented `Io`.
+//! - `client.isReusable()` tells you whether the connection survived
+//! (a ping canceled mid-flight leaves it broken; one canceled
+//! during the sleep leaves it ready).
const std = @import("std");
const clickzig = @import("clickzig");
-const SupervisorArgs = struct {
- cancel: *std.atomic.Value(bool),
- io: std.Io,
-};
-
-fn supervisor(args: SupervisorArgs) void {
- const ms_1500: std.Io.Clock.Duration = .{
- .raw = .fromMilliseconds(1_500),
+fn healthLoop(client: *clickzig.Client, io: std.Io) clickzig.ConnectError!u32 {
+ const ms_100: std.Io.Clock.Duration = .{
+ .raw = .fromMilliseconds(100),
.clock = .awake,
};
- ms_1500.sleep(args.io) catch return;
- std.debug.print("[supervisor] requesting cancel\n", .{});
- args.cancel.store(true, .release);
+ var pings: u32 = 0;
+ while (true) {
+ client.ping() catch |e| switch (e) {
+ error.Canceled => return pings,
+ else => return e,
+ };
+ pings += 1;
+ ms_100.sleep(io) catch return pings;
+ }
}
pub fn main(init: std.process.Init) !void {
const allocator = init.gpa;
const io = init.io;
- var cancel: std.atomic.Value(bool) = .init(false);
- const sup = try std.Thread.spawn(.{}, supervisor, .{SupervisorArgs{ .cancel = &cancel, .io = io }});
- defer sup.join();
-
const client = try clickzig.Client.connectTcp(.{
.control_allocator = allocator,
.read_buffer_size = 64 * 1024,
.write_buffer_size = 4 * 1024,
.username = "default",
.password = "test",
- }, io, &cancel, null);
+ }, io, null);
defer client.close();
- const ms_100: std.Io.Clock.Duration = .{
- .raw = .fromMilliseconds(100),
+ var task = try io.concurrent(healthLoop, .{ client, io });
+
+ const ms_1500: std.Io.Clock.Duration = .{
+ .raw = .fromMilliseconds(1_500),
.clock = .awake,
};
- var i: u32 = 0;
- while (true) : (i += 1) {
- client.ping(&cancel) catch |e| switch (e) {
- error.Cancelled => {
- std.debug.print("[loop] cancelled after {d} pings\n", .{i});
- break;
- },
- else => return e,
- };
- ms_100.sleep(io) catch break;
- }
+ try ms_1500.sleep(io);
+ std.debug.print("[main] canceling health loop\n", .{});
- std.debug.print("[loop] reusable={}, broken={}\n", .{ client.isReusable(), client.is_broken });
+ const pings = try task.cancel(io);
+ std.debug.print("[main] loop stopped after {d} pings; reusable={}\n", .{ pings, client.isReusable() });
}
diff --git a/examples/05_custom_transport.zig b/examples/05_custom_transport.zig
index 7cfac5d..6c67cca 100644
--- a/examples/05_custom_transport.zig
+++ b/examples/05_custom_transport.zig
@@ -45,8 +45,12 @@ const MemTransport = struct {
fn cImpl(_: *anyopaque) void {}
fn srtImpl(_: *anyopaque, _: ?u32) anyerror!void {}
fn swtImpl(_: *anyopaque, _: ?u32) anyerror!void {}
- fn lreImpl(_: *anyopaque) ?anyerror { return null; }
- fn lweImpl(_: *anyopaque) ?anyerror { return null; }
+ fn lreImpl(_: *anyopaque) ?anyerror {
+ return null;
+ }
+ fn lweImpl(_: *anyopaque) ?anyerror {
+ return null;
+ }
const vtable: clickzig.Transport.VTable = .{
.reader = rImpl,
@@ -107,7 +111,7 @@ pub fn main(init: std.process.Init) !void {
.control_allocator = allocator,
.read_buffer_size = 4096,
.write_buffer_size = 4096,
- }, std.Io.Threaded.global_single_threaded.io(), mem.transport(), null, null);
+ }, std.Io.Threaded.global_single_threaded.io(), mem.transport(), null);
defer client.close();
std.debug.print("handshake against canned bytes complete\n", .{});
diff --git a/examples/06_concurrent_queries.zig b/examples/06_concurrent_queries.zig
new file mode 100644
index 0000000..ff54b48
--- /dev/null
+++ b/examples/06_concurrent_queries.zig
@@ -0,0 +1,104 @@
+//! Example 06 — Concurrent queries with `std.Io`.
+//!
+//! 1. Fan out several queries over a `Pool` with an `Io.Group`. Each
+//! task acquires its own connection (a Client serves one in-flight
+//! call at a time; the native protocol has no multiplexing).
+//! 2. Put a deadline on a slow query: run it as a `Future`, sleep for
+//! the budget, then `cancel`. clickzig sends the protocol `Cancel`
+//! packet, so the server stops working too, and the connection goes
+//! back to the pool ready for reuse.
+//!
+//! The code only talks to `std.Io`, so it runs unchanged on any `Io`
+//! implementation that supports networking (`Io.Threaded` today).
+
+const std = @import("std");
+const clickzig = @import("clickzig");
+
+const Io = std.Io;
+
+const Job = struct {
+ table: []const u8,
+ rows: ?u64 = null,
+ err: ?anyerror = null,
+};
+
+fn countRows(pool: *clickzig.Pool, gpa: std.mem.Allocator, job: *Job) Io.Cancelable!void {
+ countRowsInner(pool, gpa, job) catch |e| switch (e) {
+ error.Canceled => return error.Canceled,
+ else => job.err = e,
+ };
+}
+
+fn countRowsInner(pool: *clickzig.Pool, gpa: std.mem.Allocator, job: *Job) !void {
+ const client = try pool.acquire();
+ defer pool.release(client);
+
+ var arena: std.heap.ArenaAllocator = .init(gpa);
+ defer arena.deinit();
+ const sql = try arena.allocator().print("SELECT count() FROM {s}", .{job.table});
+
+ var stream = try client.query(sql, arena.allocator(), .{});
+ while (try stream.next()) |packet| switch (packet) {
+ .data => |b| if (b.num_rows > 0) {
+ job.rows = b.columns[0].column.UInt64[0];
+ },
+ .exception => return error.ServerException,
+ .end_of_stream => break,
+ else => {},
+ };
+}
+
+fn slowQuery(pool: *clickzig.Pool, gpa: std.mem.Allocator) !u64 {
+ var job: Job = .{ .table = "numbers(100000000000)" };
+ try countRowsInner(pool, gpa, &job);
+ return job.rows orelse 0;
+}
+
+pub fn main(init: std.process.Init) !void {
+ const gpa = init.gpa;
+ const io = init.io;
+
+ const pool = try clickzig.Pool.init(gpa, io, .{
+ .control_allocator = gpa,
+ .read_buffer_size = 64 * 1024,
+ .write_buffer_size = 4 * 1024,
+ .username = "default",
+ .password = "test",
+ }, .{ .max_size = 4 });
+ defer pool.deinit();
+
+ // 1. Fan-out.
+ var jobs = [_]Job{
+ .{ .table = "system.tables" },
+ .{ .table = "system.columns" },
+ .{ .table = "system.functions" },
+ .{ .table = "system.settings" },
+ .{ .table = "system.data_type_families" },
+ .{ .table = "system.table_functions" },
+ };
+ var group: Io.Group = .init;
+ defer group.cancel(io);
+ for (&jobs) |*job| group.async(io, countRows, .{ pool, gpa, job });
+ try group.await(io);
+
+ for (jobs) |job| {
+ if (job.err) |e| {
+ std.debug.print("{s}: error {t}\n", .{ job.table, e });
+ } else {
+ std.debug.print("{s}: {d} rows\n", .{ job.table, job.rows.? });
+ }
+ }
+
+ // 2. Deadline: give a very slow query 500ms, then cancel it. Use
+ // `io.concurrent`, not `io.async`: `async` may run the task
+ // inline to completion when no worker is free.
+ var slow = try io.concurrent(slowQuery, .{ pool, gpa });
+ const budget: Io.Clock.Duration = .{ .raw = .fromMilliseconds(500), .clock = .awake };
+ try budget.sleep(io);
+ if (slow.cancel(io)) |n| {
+ std.debug.print("slow query finished early: {d}\n", .{n});
+ } else |e| switch (e) {
+ error.Canceled => std.debug.print("slow query canceled after 500ms; pool idle={d}\n", .{pool.idleCount()}),
+ else => return e,
+ }
+}
diff --git a/experimental/legacy_examples/async_query.zig b/experimental/legacy_examples/async_query.zig
deleted file mode 100644
index ae53aaf..0000000
--- a/experimental/legacy_examples/async_query.zig
+++ /dev/null
@@ -1,47 +0,0 @@
-const std = @import("std");
-const AsyncClient = @import("../src/async_client.zig").AsyncClient;
-const ClickHouseConfig = @import("../src/main.zig").ClickHouseConfig;
-const AsyncQuery = @import("../src/async_query.zig").AsyncQuery;
-
-pub fn main() !void {
- var gpa = std.heap.GeneralPurposeAllocator(.{}){};
- defer _ = gpa.deinit();
- const allocator = gpa.allocator();
-
- const config = ClickHouseConfig{
- .host = "localhost",
- .port = 9000,
- .username = "default",
- .password = "",
- .database = "default",
- };
-
- var client = AsyncClient.init(allocator, config);
- defer client.deinit();
-
- try client.connect();
-
- // Create async query
- var query = AsyncQuery.init(
- allocator,
- &client,
- "SELECT number FROM system.numbers LIMIT 10000000"
- );
- defer query.deinit();
-
- // Execute query with callback
- try query.executeWithCallback(queryCallback);
-
- std.debug.print("Async query started\n", .{});
-}
-
-fn queryCallback(query: *AsyncQuery, result: ?*results.QueryResult, err: ?*error.Error) void {
- if (err) |e| {
- std.debug.print("Query error: {}\n", .{e});
- return;
- }
-
- if (result) |r| {
- std.debug.print("Query completed with {} rows\n", .{r.rows});
- }
-}
\ No newline at end of file
diff --git a/experimental/legacy_examples/basic_connection.zig b/experimental/legacy_examples/basic_connection.zig
deleted file mode 100644
index 6121ebb..0000000
--- a/experimental/legacy_examples/basic_connection.zig
+++ /dev/null
@@ -1,23 +0,0 @@
-const std = @import("std");
-const ClickHouseClient = @import("../src/main.zig").ClickHouseClient;
-const ClickHouseConfig = @import("../src/main.zig").ClickHouseConfig;
-
-pub fn main() !void {
- var gpa = std.heap.GeneralPurposeAllocator(.{}){};
- defer _ = gpa.deinit();
- const allocator = gpa.allocator();
-
- const config = ClickHouseConfig{
- .host = "localhost",
- .port = 9000,
- .username = "default",
- .password = "",
- .database = "default",
- };
-
- var client = ClickHouseClient.init(allocator, config);
- defer client.deinit();
-
- try client.connect();
- std.debug.print("Successfully connected to ClickHouse server!\n", .{});
-}
\ No newline at end of file
diff --git a/experimental/legacy_examples/bulk_insert.zig b/experimental/legacy_examples/bulk_insert.zig
deleted file mode 100644
index f7c73d2..0000000
--- a/experimental/legacy_examples/bulk_insert.zig
+++ /dev/null
@@ -1,66 +0,0 @@
-const std = @import("std");
-const ClickHouseClient = @import("../src/main.zig").ClickHouseClient;
-const ClickHouseConfig = @import("../src/main.zig").ClickHouseConfig;
-const BulkInsert = @import("../src/bulk_insert.zig").BulkInsert;
-const compression = @import("../src/compression.zig");
-
-pub fn main() !void {
- var gpa = std.heap.GeneralPurposeAllocator(.{}){};
- defer _ = gpa.deinit();
- const allocator = gpa.allocator();
-
- const config = ClickHouseConfig{
- .host = "localhost",
- .port = 9000,
- .username = "default",
- .password = "",
- .database = "default",
- };
-
- var client = ClickHouseClient.init(allocator, config);
- defer client.deinit();
-
- try client.connect();
-
- // Create test table
- try client.query(
- \\CREATE TABLE IF NOT EXISTS test_bulk (
- \\ id UInt32,
- \\ name String,
- \\ value Float64
- \\) ENGINE = MergeTree()
- \\ORDER BY id
- );
-
- // Initialize bulk insert
- const columns = [_]BulkInsert.ColumnDef{
- .{ .name = "id", .type_str = "UInt32" },
- .{ .name = "name", .type_str = "String" },
- .{ .name = "value", .type_str = "Float64" },
- };
-
- var bulk = try BulkInsert.init(allocator, "test_bulk", &columns, 1000);
- defer bulk.deinit();
-
- // Enable LZ4 compression
- bulk.setCompression(.LZ4);
-
- // Insert sample data
- var i: u32 = 0;
- while (i < 10000) : (i += 1) {
- const values = [_]BulkInsert.Value{
- .{ .UInt32 = i },
- .{ .String = "test" },
- .{ .Float64 = @intToFloat(f64, i) * 1.5 },
- };
-
- if (try bulk.addRow(&values)) {
- try bulk.flush();
- }
- }
-
- // Flush any remaining rows
- try bulk.flush();
-
- std.debug.print("Bulk insert completed successfully\n", .{});
-}
\ No newline at end of file
diff --git a/experimental/legacy_examples/complex_types.zig b/experimental/legacy_examples/complex_types.zig
deleted file mode 100644
index 06842d6..0000000
--- a/experimental/legacy_examples/complex_types.zig
+++ /dev/null
@@ -1,122 +0,0 @@
-const std = @import("std");
-const ClickHouseClient = @import("../src/main.zig").ClickHouseClient;
-const ClickHouseConfig = @import("../src/main.zig").ClickHouseConfig;
-const complex_types = @import("../src/complex_types.zig");
-const BulkInsert = @import("../src/bulk_insert.zig").BulkInsert;
-
-pub fn main() !void {
- var gpa = std.heap.GeneralPurposeAllocator(.{}){};
- defer _ = gpa.deinit();
- const allocator = gpa.allocator();
-
- const config = ClickHouseConfig{
- .host = "localhost",
- .port = 9000,
- .username = "default",
- .password = "",
- .database = "default",
- };
-
- var client = ClickHouseClient.init(allocator, config);
- defer client.deinit();
-
- try client.connect();
-
- // Create table with complex types
- try client.query(
- \\CREATE TABLE IF NOT EXISTS test_complex (
- \\ id UInt32,
- \\ tags Array(String),
- \\ properties Map(String, String),
- \\ nested Nested(
- \\ key String,
- \\ value Float64
- \\ ),
- \\ category LowCardinality(String),
- \\ location Point,
- \\ area Polygon,
- \\ metadata JSON
- \\) ENGINE = MergeTree()
- \\ORDER BY id
- );
-
- // Prepare complex data types
- var tags = [_][]const u8{ "tag1", "tag2", "tag3" };
-
- var properties = std.StringHashMap([]const u8).init(allocator);
- defer properties.deinit();
- try properties.put("color", "red");
- try properties.put("size", "large");
-
- var nested = complex_types.Nested.init(
- allocator,
- "metrics",
- &[_]types.TypeInfo{
- .{ .base_type = .String },
- .{ .base_type = .Float64 },
- },
- );
- defer nested.deinit();
-
- var location = complex_types.Point{
- .x = 10.5,
- .y = 20.7,
- };
-
- var area = try complex_types.Polygon.init(
- allocator,
- &[_]complex_types.Ring{
- try complex_types.Ring.init(allocator, &[_]complex_types.Point{
- .{ .x = 0, .y = 0 },
- .{ .x = 10, .y = 0 },
- .{ .x = 10, .y = 10 },
- .{ .x = 0, .y = 10 },
- .{ .x = 0, .y = 0 },
- }),
- },
- );
- defer area.deinit();
-
- var metadata = try complex_types.JSON.init(
- allocator,
- \\{
- \\ "version": 1,
- \\ "description": "Test object",
- \\ "enabled": true
- \\}
- );
- defer metadata.deinit();
-
- // Insert data using bulk insert
- const columns = [_]BulkInsert.ColumnDef{
- .{ .name = "id", .type_str = "UInt32" },
- .{ .name = "tags", .type_str = "Array(String)" },
- .{ .name = "properties", .type_str = "Map(String, String)" },
- .{ .name = "nested", .type_str = "Nested(key String, value Float64)" },
- .{ .name = "category", .type_str = "LowCardinality(String)" },
- .{ .name = "location", .type_str = "Point" },
- .{ .name = "area", .type_str = "Polygon" },
- .{ .name = "metadata", .type_str = "JSON" },
- };
-
- var bulk = try BulkInsert.init(allocator, "test_complex", &columns, 1000);
- defer bulk.deinit();
-
- // Insert sample row
- const values = [_]BulkInsert.Value{
- .{ .UInt32 = 1 },
- .{ .Array = &tags },
- .{ .Map = properties },
- .{ .Nested = nested },
- .{ .String = "category_a" },
- .{ .Point = location },
- .{ .Polygon = area },
- .{ .JSON = metadata },
- };
-
- if (try bulk.addRow(&values)) {
- try bulk.flush();
- }
-
- std.debug.print("Complex types example completed successfully\n", .{});
-}
\ No newline at end of file
diff --git a/experimental/legacy_examples/compression.zig b/experimental/legacy_examples/compression.zig
deleted file mode 100644
index fa6f95f..0000000
--- a/experimental/legacy_examples/compression.zig
+++ /dev/null
@@ -1,106 +0,0 @@
-const std = @import("std");
-const ClickHouseClient = @import("../src/main.zig").ClickHouseClient;
-const ClickHouseConfig = @import("../src/main.zig").ClickHouseConfig;
-const compression = @import("../src/compression.zig");
-const BulkInsert = @import("../src/bulk_insert.zig").BulkInsert;
-
-pub fn main() !void {
- var gpa = std.heap.GeneralPurposeAllocator(.{}){};
- defer _ = gpa.deinit();
- const allocator = gpa.allocator();
-
- const config = ClickHouseConfig{
- .host = "localhost",
- .port = 9000,
- .username = "default",
- .password = "",
- .database = "default",
- };
-
- var client = ClickHouseClient.init(allocator, config);
- defer client.deinit();
-
- try client.connect();
-
- // Create test table
- try client.query(
- \\CREATE TABLE IF NOT EXISTS test_compression (
- \\ id UInt32,
- \\ data String,
- \\ numbers Array(Float64)
- \\) ENGINE = MergeTree()
- \\ORDER BY id
- );
-
- // Test different compression methods
- const compression_methods = [_]compression.CompressionMethod{
- .None,
- .LZ4,
- .ZSTD,
- };
-
- // Generate sample data
- var large_string = std.ArrayList(u8).init(allocator);
- defer large_string.deinit();
- var i: usize = 0;
- while (i < 10000) : (i += 1) {
- try large_string.writer().print("This is test data line {}\n", .{i});
- }
-
- var numbers = std.ArrayList(f64).init(allocator);
- defer numbers.deinit();
- i = 0;
- while (i < 1000) : (i += 1) {
- try numbers.append(@intToFloat(f64, i) * 1.5);
- }
-
- // Test each compression method
- for (compression_methods) |method| {
- // Initialize bulk insert with compression
- const columns = [_]BulkInsert.ColumnDef{
- .{ .name = "id", .type_str = "UInt32" },
- .{ .name = "data", .type_str = "String" },
- .{ .name = "numbers", .type_str = "Array(Float64)" },
- };
-
- var bulk = try BulkInsert.init(allocator, "test_compression", &columns, 1000);
- defer bulk.deinit();
-
- // Set compression method
- bulk.setCompression(method);
-
- // Insert data
- const values = [_]BulkInsert.Value{
- .{ .UInt32 = 1 },
- .{ .String = large_string.items },
- .{ .Array = numbers.items },
- };
-
- const start = std.time.milliTimestamp();
-
- if (try bulk.addRow(&values)) {
- try bulk.flush();
- }
-
- const end = std.time.milliTimestamp();
-
- // Compress the data separately to measure compression ratio
- const compressed = try compression.CompressedData.compress(
- allocator,
- large_string.items,
- method,
- );
- defer compressed.deinit(allocator);
-
- const ratio = @intToFloat(f64, large_string.items.len) /
- @intToFloat(f64, compressed.compressed_size);
-
- std.debug.print("\nCompression Method: {s}\n", .{@tagName(method)});
- std.debug.print("Time: {}ms\n", .{end - start});
- std.debug.print("Original Size: {} bytes\n", .{large_string.items.len});
- std.debug.print("Compressed Size: {} bytes\n", .{compressed.compressed_size});
- std.debug.print("Compression Ratio: {d:.2}x\n", .{ratio});
- }
-
- std.debug.print("\nCompression example completed successfully\n", .{});
-}
\ No newline at end of file
diff --git a/experimental/legacy_examples/database_size.zig b/experimental/legacy_examples/database_size.zig
deleted file mode 100644
index c7b926b..0000000
--- a/experimental/legacy_examples/database_size.zig
+++ /dev/null
@@ -1,37 +0,0 @@
-const std = @import("std");
-const ClickHouseClient = @import("../src/main.zig").ClickHouseClient;
-const ClickHouseConfig = @import("../src/main.zig").ClickHouseConfig;
-
-pub fn main() !void {
- var gpa = std.heap.GeneralPurposeAllocator(.{}){};
- defer _ = gpa.deinit();
- const allocator = gpa.allocator();
-
- const config = ClickHouseConfig{
- .host = "localhost",
- .port = 9000,
- .username = "default",
- .password = "",
- .database = "default",
- };
-
- var client = ClickHouseClient.init(allocator, config);
- defer client.deinit();
-
- try client.connect();
-
- // Query to get database sizes in GB
- const size_query =
- \\SELECT
- \\ database,
- \\ formatReadableSize(sum(bytes)) AS size,
- \\ round(sum(bytes) / pow(1024, 3), 2) AS size_gb
- \\FROM system.parts
- \\GROUP BY database
- \\ORDER BY sum(bytes) DESC
- ;
-
- try client.query(size_query);
-
- std.debug.print("Query executed: Database sizes retrieved\n", .{});
-}
\ No newline at end of file
diff --git a/experimental/legacy_examples/dictionary.zig b/experimental/legacy_examples/dictionary.zig
deleted file mode 100644
index 7fa5290..0000000
--- a/experimental/legacy_examples/dictionary.zig
+++ /dev/null
@@ -1,60 +0,0 @@
-const std = @import("std");
-const ClickHouseClient = @import("../src/main.zig").ClickHouseClient;
-const ClickHouseConfig = @import("../src/main.zig").ClickHouseConfig;
-const Dictionary = @import("../src/dictionary.zig").Dictionary;
-
-pub fn main() !void {
- var gpa = std.heap.GeneralPurposeAllocator(.{}){};
- defer _ = gpa.deinit();
- const allocator = gpa.allocator();
-
- const config = ClickHouseConfig{
- .host = "localhost",
- .port = 9000,
- .username = "default",
- .password = "",
- .database = "default",
- };
-
- var client = ClickHouseClient.init(allocator, config);
- defer client.deinit();
-
- try client.connect();
-
- // Create source table for dictionary
- try client.query(
- \\CREATE TABLE IF NOT EXISTS user_data (
- \\ user_id UInt32,
- \\ name String,
- \\ country_code String,
- \\ created_at DateTime
- \\) ENGINE = MergeTree()
- \\ORDER BY user_id
- );
-
- // Define dictionary structure
- const attributes = [_]Dictionary.DictionaryAttribute{
- .{ .name = "name", .type = "String" },
- .{ .name = "country_code", .type = "String" },
- .{ .name = "created_at", .type = "DateTime" },
- };
-
- // Create dictionary
- var dict = try Dictionary.init(
- allocator,
- "user_dictionary",
- .{ .ClickHouse = .{
- .db = "default",
- .table = "user_data",
- }},
- .ComplexKeyHashTable,
- .{ .TTL = 3600 }, // 1 hour
- &attributes
- );
- defer dict.deinit();
-
- // Create the dictionary
- try dict.create(&client);
-
- std.debug.print("Dictionary created successfully\n", .{});
-}
\ No newline at end of file
diff --git a/experimental/legacy_examples/distributed_table.zig b/experimental/legacy_examples/distributed_table.zig
deleted file mode 100644
index 3c8c278..0000000
--- a/experimental/legacy_examples/distributed_table.zig
+++ /dev/null
@@ -1,53 +0,0 @@
-const std = @import("std");
-const ClickHouseClient = @import("../src/main.zig").ClickHouseClient;
-const ClickHouseConfig = @import("../src/main.zig").ClickHouseConfig;
-const DistributedTable = @import("../src/distributed.zig").DistributedTable;
-
-pub fn main() !void {
- var gpa = std.heap.GeneralPurposeAllocator(.{}){};
- defer _ = gpa.deinit();
- const allocator = gpa.allocator();
-
- const config = ClickHouseConfig{
- .host = "localhost",
- .port = 9000,
- .username = "default",
- .password = "",
- .database = "default",
- };
-
- var client = ClickHouseClient.init(allocator, config);
- defer client.deinit();
-
- try client.connect();
-
- // Create local table first
- try client.query(
- \\CREATE TABLE IF NOT EXISTS events_local (
- \\ event_time DateTime,
- \\ user_id UInt32,
- \\ event_type String,
- \\ value Float64
- \\) ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/events', '{replica}')
- \\PARTITION BY toYYYYMM(event_time)
- \\ORDER BY (event_time, user_id)
- );
-
- // Create distributed table
- var dist_table = try DistributedTable.init(
- allocator,
- "events_distributed",
- "my_cluster",
- "default",
- "events_local"
- );
- defer dist_table.deinit();
-
- // Set sharding key
- try dist_table.setShardingKey("cityHash64(user_id)");
-
- // Create the distributed table
- try dist_table.create(&client);
-
- std.debug.print("Distributed table created successfully\n", .{});
-}
\ No newline at end of file
diff --git a/experimental/legacy_examples/materialized_view.zig b/experimental/legacy_examples/materialized_view.zig
deleted file mode 100644
index 575a721..0000000
--- a/experimental/legacy_examples/materialized_view.zig
+++ /dev/null
@@ -1,55 +0,0 @@
-const std = @import("std");
-const ClickHouseClient = @import("../src/main.zig").ClickHouseClient;
-const ClickHouseConfig = @import("../src/main.zig").ClickHouseConfig;
-const MaterializedView = @import("../src/views.zig").MaterializedView;
-
-pub fn main() !void {
- var gpa = std.heap.GeneralPurposeAllocator(.{}){};
- defer _ = gpa.deinit();
- const allocator = gpa.allocator();
-
- const config = ClickHouseConfig{
- .host = "localhost",
- .port = 9000,
- .username = "default",
- .password = "",
- .database = "default",
- };
-
- var client = ClickHouseClient.init(allocator, config);
- defer client.deinit();
-
- try client.connect();
-
- // Create source table
- try client.query(
- \\CREATE TABLE IF NOT EXISTS events (
- \\ timestamp DateTime,
- \\ user_id UInt32,
- \\ event_type String,
- \\ value Float64
- \\) ENGINE = MergeTree()
- \\ORDER BY (timestamp, user_id)
- );
-
- // Create materialized view
- var view = try MaterializedView.init(
- allocator,
- "events_hourly",
- "events_hourly_table",
- \\SELECT
- \\ toStartOfHour(timestamp) as hour,
- \\ user_id,
- \\ event_type,
- \\ count() as event_count,
- \\ sum(value) as total_value
- \\FROM events
- \\GROUP BY hour, user_id, event_type
- );
- defer view.deinit();
-
- // Create the view
- try view.create(&client);
-
- std.debug.print("Materialized view created successfully\n", .{});
-}
\ No newline at end of file
diff --git a/experimental/legacy_examples/mutations.zig b/experimental/legacy_examples/mutations.zig
deleted file mode 100644
index 4593ec9..0000000
--- a/experimental/legacy_examples/mutations.zig
+++ /dev/null
@@ -1,61 +0,0 @@
-const std = @import("std");
-const ClickHouseClient = @import("../src/main.zig").ClickHouseClient;
-const ClickHouseConfig = @import("../src/main.zig").ClickHouseConfig;
-const Mutation = @import("../src/mutations.zig").Mutation;
-
-pub fn main() !void {
- var gpa = std.heap.GeneralPurposeAllocator(.{}){};
- defer _ = gpa.deinit();
- const allocator = gpa.allocator();
-
- const config = ClickHouseConfig{
- .host = "localhost",
- .port = 9000,
- .username = "default",
- .password = "",
- .database = "default",
- };
-
- var client = ClickHouseClient.init(allocator, config);
- defer client.deinit();
-
- try client.connect();
-
- // Create test table
- try client.query(
- \\CREATE TABLE IF NOT EXISTS test_mutations (
- \\ id UInt32,
- \\ name String,
- \\ value Float64,
- \\ status String
- \\) ENGINE = MergeTree()
- \\ORDER BY id
- );
-
- // Create mutation
- var mutation = try Mutation.init(allocator, "test_mutations", .Update);
- defer mutation.deinit();
-
- // Set mutation parameters
- try mutation.setCondition("status = 'pending'");
- try mutation.setValue("status", "'completed'");
- try mutation.setValue("value", "value * 2");
-
- // Execute mutation
- const mutation_id = try mutation.execute(&client);
-
- // Monitor mutation status
- while (true) {
- const status = try mutation.status(&client, mutation_id);
- if (status.is_done) {
- std.debug.print("Mutation completed successfully\n", .{});
- break;
- }
- if (status.failed_part != null) {
- std.debug.print("Mutation failed: {s}\n", .{status.fail_reason.?});
- break;
- }
- std.debug.print("Mutation in progress: {} parts remaining\n", .{status.parts_to_do});
- std.time.sleep(1 * std.time.ns_per_s);
- }
-}
\ No newline at end of file
diff --git a/experimental/legacy_examples/pool_config.zig b/experimental/legacy_examples/pool_config.zig
deleted file mode 100644
index 7062f34..0000000
--- a/experimental/legacy_examples/pool_config.zig
+++ /dev/null
@@ -1,54 +0,0 @@
-const std = @import("std");
-const ClickHouseClient = @import("../src/main.zig").ClickHouseClient;
-const ClickHouseConfig = @import("../src/main.zig").ClickHouseConfig;
-const Pool = @import("../src/pool.zig").Pool;
-const PoolConfig = @import("../src/pool_config.zig").PoolConfig;
-const RetryStrategy = @import("../src/retry.zig").RetryStrategy;
-
-pub fn main() !void {
- var gpa = std.heap.GeneralPurposeAllocator(.{}){};
- defer _ = gpa.deinit();
- const allocator = gpa.allocator();
-
- const config = ClickHouseConfig{
- .host = "localhost",
- .port = 9000,
- .username = "default",
- .password = "",
- .database = "default",
- };
-
- // Configure connection pool
- var pool_config = PoolConfig.init();
- pool_config.setMinConnections(5);
- pool_config.setMaxConnections(20);
- pool_config.setConnectionTimeout(3000);
- pool_config.setMaxIdleTime(300000);
- pool_config.setHealthCheckInterval(30000);
- pool_config.setMaxWaitQueueSize(50);
-
- // Configure retry strategy
- var retry_strategy = RetryStrategy{
- .max_attempts = 3,
- .initial_delay_ms = 100,
- .max_delay_ms = 5000,
- .multiplier = 2.0,
- .jitter = true,
- };
- pool_config.setRetryStrategy(retry_strategy);
-
- // Configure TCP settings
- pool_config.setTcpKeepalive(true);
- pool_config.setTcpNodelay(true);
-
- // Create pool
- var pool = try Pool.init(allocator, config, pool_config);
- defer pool.deinit();
-
- // Acquire connection and execute query
- var client = try pool.acquire();
- try client.query("SELECT 1");
- pool.release(client);
-
- std.debug.print("Pool configuration example completed successfully\n", .{});
-}
\ No newline at end of file
diff --git a/experimental/legacy_examples/pool_example.zig b/experimental/legacy_examples/pool_example.zig
deleted file mode 100644
index 7403a53..0000000
--- a/experimental/legacy_examples/pool_example.zig
+++ /dev/null
@@ -1,36 +0,0 @@
-const std = @import("std");
-const Pool = @import("../src/pool.zig").Pool;
-const PoolConfig = @import("../src/pool.zig").PoolConfig;
-const ClickHouseConfig = @import("../src/main.zig").ClickHouseConfig;
-
-pub fn main() !void {
- var gpa = std.heap.GeneralPurposeAllocator(.{}){};
- defer _ = gpa.deinit();
- const allocator = gpa.allocator();
-
- const config = ClickHouseConfig{
- .host = "localhost",
- .port = 9000,
- .username = "default",
- .password = "",
- .database = "default",
- };
-
- const pool_config = PoolConfig{
- .min_connections = 2,
- .max_connections = 5,
- .connection_timeout_ms = 5000,
- };
-
- var pool = try Pool.init(allocator, config, pool_config);
- defer pool.deinit();
-
- // Acquire a connection
- var client = try pool.acquire();
- try client.query("SELECT 1");
-
- // Release the connection back to the pool
- pool.release(client);
-
- std.debug.print("Pool example completed successfully\n", .{});
-}
\ No newline at end of file
diff --git a/experimental/legacy_examples/query_cancel.zig b/experimental/legacy_examples/query_cancel.zig
deleted file mode 100644
index 0e50010..0000000
--- a/experimental/legacy_examples/query_cancel.zig
+++ /dev/null
@@ -1,49 +0,0 @@
-const std = @import("std");
-const ClickHouseClient = @import("../src/main.zig").ClickHouseClient;
-const ClickHouseConfig = @import("../src/main.zig").ClickHouseConfig;
-const QueryContext = @import("../src/query.zig").QueryContext;
-
-pub fn main() !void {
- var gpa = std.heap.GeneralPurposeAllocator(.{}){};
- defer _ = gpa.deinit();
- const allocator = gpa.allocator();
-
- const config = ClickHouseConfig{
- .host = "localhost",
- .port = 9000,
- .username = "default",
- .password = "",
- .database = "default",
- };
-
- var client = ClickHouseClient.init(allocator, config);
- defer client.deinit();
-
- try client.connect();
-
- // Create query context
- var ctx = try QueryContext.init(allocator);
- defer ctx.deinit();
-
- // Start a long-running query
- const query = "SELECT sleep(10)";
-
- // Spawn a thread to cancel the query after 2 seconds
- const thread = try std.Thread.spawn(.{}, struct {
- fn run(token: *QueryContext) void {
- std.time.sleep(2 * std.time.ns_per_s);
- token.cancel_token.cancel();
- }
- }.run, .{ctx});
-
- // Execute query with cancellation context
- client.queryWithContext(query, ctx) catch |err| {
- if (err == error.QueryCancelled) {
- std.debug.print("Query was cancelled as expected\n", .{});
- } else {
- return err;
- }
- };
-
- thread.join();
-}
\ No newline at end of file
diff --git a/experimental/legacy_examples/query_control.zig b/experimental/legacy_examples/query_control.zig
deleted file mode 100644
index f5b3e3d..0000000
--- a/experimental/legacy_examples/query_control.zig
+++ /dev/null
@@ -1,65 +0,0 @@
-const std = @import("std");
-const ClickHouseClient = @import("../src/main.zig").ClickHouseClient;
-const ClickHouseConfig = @import("../src/main.zig").ClickHouseConfig;
-const QueryControl = @import("../src/query_control.zig").QueryControl;
-
-pub fn main() !void {
- var gpa = std.heap.GeneralPurposeAllocator(.{}){};
- defer _ = gpa.deinit();
- const allocator = gpa.allocator();
-
- const config = ClickHouseConfig{
- .host = "localhost",
- .port = 9000,
- .username = "default",
- .password = "",
- .database = "default",
- };
-
- var client = ClickHouseClient.init(allocator, config);
- defer client.deinit();
-
- try client.connect();
-
- // Create query control with timeout
- var ctrl = try QueryControl.init(&client, allocator, 5000);
- defer ctrl.deinit();
-
- // Start a long-running query in a separate thread
- const thread = try std.Thread.spawn(.{}, struct {
- fn run(c: *ClickHouseClient) !void {
- try c.query(
- \\SELECT
- \\ number,
- \\ sleep(0.1)
- \\FROM system.numbers
- \\LIMIT 100
- );
- }
- }.run, .{&client});
-
- // Monitor query progress
- while (true) {
- if (ctrl.isTimeout()) {
- std.debug.print("Query timeout, cancelling...\n", .{});
- try ctrl.cancel();
- break;
- }
-
- const progress = try ctrl.getProgress();
- std.debug.print(
- "Progress: {d:.2}% ({} rows, {} bytes)\n",
- .{
- progress.progress * 100,
- progress.read_rows,
- progress.read_bytes,
- },
- );
-
- if (progress.progress >= 1.0) break;
- std.time.sleep(100 * std.time.ns_per_ms);
- }
-
- thread.join();
- std.debug.print("Query control example completed\n", .{});
-}
\ No newline at end of file
diff --git a/experimental/legacy_examples/query_profiling.zig b/experimental/legacy_examples/query_profiling.zig
deleted file mode 100644
index 71b5a4e..0000000
--- a/experimental/legacy_examples/query_profiling.zig
+++ /dev/null
@@ -1,67 +0,0 @@
-const std = @import("std");
-const ClickHouseClient = @import("../src/main.zig").ClickHouseClient;
-const ClickHouseConfig = @import("../src/main.zig").ClickHouseConfig;
-const QueryProfile = @import("../src/profiling.zig").QueryProfile;
-
-pub fn main() !void {
- var gpa = std.heap.GeneralPurposeAllocator(.{}){};
- defer _ = gpa.deinit();
- const allocator = gpa.allocator();
-
- const config = ClickHouseConfig{
- .host = "localhost",
- .port = 9000,
- .username = "default",
- .password = "",
- .database = "default",
- };
-
- var client = ClickHouseClient.init(allocator, config);
- defer client.deinit();
-
- try client.connect();
-
- // Create a query profile
- var profile = try QueryProfile.init(
- &client,
- allocator,
- "test_query",
- .{
- .collect_memory_stats = true,
- .collect_io_stats = true,
- .track_memory_reference = true,
- }
- );
- defer profile.deinit();
-
- // Execute a test query
- try client.query(
- \\SELECT
- \\ number,
- \\ toString(number) as str,
- \\ number * 2 as double
- \\FROM system.numbers
- \\LIMIT 1000000
- );
-
- // Get execution statistics
- const stats = try profile.getExecutionStats();
- std.debug.print("\nQuery Statistics:\n", .{});
- std.debug.print("Duration: {}ms\n", .{stats.duration_ms});
- std.debug.print("Rows Read: {}\n", .{stats.read_rows});
- std.debug.print("Bytes Read: {}\n", .{stats.read_bytes});
- std.debug.print("Memory Usage: {}\n", .{stats.memory_usage});
- std.debug.print("Peak Memory: {}\n", .{stats.peak_memory_usage});
- std.debug.print("Thread Count: {}\n", .{stats.thread_count});
-
- // Get thread statistics
- const thread_stats = try profile.getThreadStats();
- std.debug.print("\nThread Statistics:\n", .{});
- for (thread_stats) |thread| {
- std.debug.print("Thread {}: {}ms, Memory: {}\n", .{
- thread.thread_id,
- thread.duration_ms,
- thread.memory_usage,
- });
- }
-}
\ No newline at end of file
diff --git a/experimental/legacy_examples/sampling.zig b/experimental/legacy_examples/sampling.zig
deleted file mode 100644
index 023117d..0000000
--- a/experimental/legacy_examples/sampling.zig
+++ /dev/null
@@ -1,96 +0,0 @@
-const std = @import("std");
-const ClickHouseClient = @import("../src/main.zig").ClickHouseClient;
-const ClickHouseConfig = @import("../src/main.zig").ClickHouseConfig;
-const sampling = @import("../src/sampling.zig");
-
-pub fn main() !void {
- var gpa = std.heap.GeneralPurposeAllocator(.{}){};
- defer _ = gpa.deinit();
- const allocator = gpa.allocator();
-
- const config = ClickHouseConfig{
- .host = "localhost",
- .port = 9000,
- .username = "default",
- .password = "",
- .database = "default",
- };
-
- var client = ClickHouseClient.init(allocator, config);
- defer client.deinit();
-
- try client.connect();
-
- // Create test table with sampling
- try client.query(
- \\CREATE TABLE IF NOT EXISTS test_sampling (
- \\ id UInt32,
- \\ name String,
- \\ value Float64
- \\) ENGINE = MergeTree()
- \\ORDER BY id
- \\SAMPLE BY cityHash64(id)
- );
-
- // Insert sample data
- try client.query(
- \\INSERT INTO test_sampling
- \\SELECT
- \\ number as id,
- \\ concat('name_', toString(number)) as name,
- \\ number * 1.5 as value
- \\FROM system.numbers
- \\LIMIT 1000000
- );
-
- // Random sampling
- const random_sample = try sampling.applySampling(
- &client,
- "SELECT * FROM test_sampling",
- .{
- .method = .Random,
- .sample_size = 0.1,
- .seed = 42,
- },
- allocator,
- );
- defer allocator.free(random_sample);
- try client.query(random_sample);
-
- // Deterministic sampling
- const deterministic_sample = try sampling.applySampling(
- &client,
- "SELECT * FROM test_sampling",
- .{
- .method = .Deterministic,
- .sample_size = 0.1,
- },
- allocator,
- );
- defer allocator.free(deterministic_sample);
- try client.query(deterministic_sample);
-
- // Stratified sampling
- var stratified = try sampling.StratifiedSampling.init(allocator, &[_]sampling.StratifiedSampling.Stratum{
- .{
- .column = "value",
- .value = "< 1000",
- .sample_size = 1000,
- },
- .{
- .column = "value",
- .value = "BETWEEN 1000 AND 10000",
- .sample_size = 500,
- },
- .{
- .column = "value",
- .value = "> 10000",
- .sample_size = 100,
- },
- });
- defer stratified.deinit();
-
- try stratified.apply(&client, "test_sampling");
-
- std.debug.print("Sampling examples completed successfully\n", .{});
-}
\ No newline at end of file
diff --git a/experimental/legacy_examples/show_connections.zig b/experimental/legacy_examples/show_connections.zig
deleted file mode 100644
index c1bc1db..0000000
--- a/experimental/legacy_examples/show_connections.zig
+++ /dev/null
@@ -1,39 +0,0 @@
-const std = @import("std");
-const ClickHouseClient = @import("../src/main.zig").ClickHouseClient;
-const ClickHouseConfig = @import("../src/main.zig").ClickHouseConfig;
-
-pub fn main() !void {
- var gpa = std.heap.GeneralPurposeAllocator(.{}){};
- defer _ = gpa.deinit();
- const allocator = gpa.allocator();
-
- const config = ClickHouseConfig{
- .host = "localhost",
- .port = 9000,
- .username = "default",
- .password = "",
- .database = "default",
- };
-
- var client = ClickHouseClient.init(allocator, config);
- defer client.deinit();
-
- try client.connect();
-
- // Query to show current connections
- const connections_query =
- \\SELECT
- \\ user,
- \\ address,
- \\ client_name,
- \\ client_version,
- \\ query_duration_ms,
- \\ query
- \\FROM system.processes
- \\ORDER BY query_duration_ms DESC
- ;
-
- try client.query(connections_query);
-
- std.debug.print("Query executed: Current connections retrieved\n", .{});
-}
\ No newline at end of file
diff --git a/experimental/legacy_examples/show_databases.zig b/experimental/legacy_examples/show_databases.zig
deleted file mode 100644
index ec406b8..0000000
--- a/experimental/legacy_examples/show_databases.zig
+++ /dev/null
@@ -1,25 +0,0 @@
-const std = @import("std");
-const ClickHouseClient = @import("../src/main.zig").ClickHouseClient;
-const ClickHouseConfig = @import("../src/main.zig").ClickHouseConfig;
-
-pub fn main() !void {
- var gpa = std.heap.GeneralPurposeAllocator(.{}){};
- defer _ = gpa.deinit();
- const allocator = gpa.allocator();
-
- const config = ClickHouseConfig{
- .host = "localhost",
- .port = 9000,
- .username = "default",
- .password = "",
- .database = "default",
- };
-
- var client = ClickHouseClient.init(allocator, config);
- defer client.deinit();
-
- try client.connect();
- try client.query("SHOW DATABASES");
-
- std.debug.print("Query executed: SHOW DATABASES\n", .{});
-}
\ No newline at end of file
diff --git a/experimental/legacy_examples/streaming.zig b/experimental/legacy_examples/streaming.zig
deleted file mode 100644
index 8c0fe74..0000000
--- a/experimental/legacy_examples/streaming.zig
+++ /dev/null
@@ -1,92 +0,0 @@
-const std = @import("std");
-const ClickHouseClient = @import("../src/main.zig").ClickHouseClient;
-const ClickHouseConfig = @import("../src/main.zig").ClickHouseConfig;
-const RowStream = @import("../src/stream.zig").RowStream;
-const StreamBuffer = @import("../src/stream_buffer.zig").StreamBuffer;
-
-pub fn main() !void {
- var gpa = std.heap.GeneralPurposeAllocator(.{}){};
- defer _ = gpa.deinit();
- const allocator = gpa.allocator();
-
- const config = ClickHouseConfig{
- .host = "localhost",
- .port = 9000,
- .username = "default",
- .password = "",
- .database = "default",
- };
-
- var client = ClickHouseClient.init(allocator, config);
- defer client.deinit();
-
- try client.connect();
-
- // Create test table
- try client.query(
- \\CREATE TABLE IF NOT EXISTS test_stream (
- \\ id UInt32,
- \\ name String,
- \\ value Float64
- \\) ENGINE = MergeTree()
- \\ORDER BY id
- );
-
- // Insert sample data
- try client.query(
- \\INSERT INTO test_stream
- \\SELECT
- \\ number as id,
- \\ concat('name_', toString(number)) as name,
- \\ number * 1.5 as value
- \\FROM system.numbers
- \\LIMIT 1000000
- );
-
- // Create row stream with buffer
- var stream = RowStream.init(allocator, 10000);
- defer stream.deinit();
-
- // Create stream buffer
- var buffer = StreamBuffer.init(allocator);
- defer buffer.deinit();
- buffer.setCapacity(5000);
-
- // Start streaming query
- const query = "SELECT * FROM test_stream WHERE id % 100 = 0";
- var iterator = stream.iterator();
-
- // Process rows in chunks
- var row_count: usize = 0;
- while (try iterator.next()) |row| {
- // Buffer the row
- try buffer.push(row);
- row_count += 1;
-
- // Process buffered rows when buffer is full
- if (buffer.size >= buffer.capacity) {
- try processBufferedRows(&buffer);
- }
- }
-
- // Process any remaining rows
- if (buffer.size > 0) {
- try processBufferedRows(&buffer);
- }
-
- std.debug.print("Processed {} rows using streaming\n", .{row_count});
-}
-
-fn processBufferedRows(buffer: *StreamBuffer) !void {
- while (buffer.pop()) |row| {
- // Process each row
- const id = try row.getUInt32("id");
- const name = try row.getString("name");
- const value = try row.getFloat64("value");
-
- // Do something with the row data...
- _ = id;
- _ = name;
- _ = value;
- }
-}
\ No newline at end of file
diff --git a/experimental/legacy_examples/transaction.zig b/experimental/legacy_examples/transaction.zig
deleted file mode 100644
index 9473ba0..0000000
--- a/experimental/legacy_examples/transaction.zig
+++ /dev/null
@@ -1,49 +0,0 @@
-const std = @import("std");
-const ClickHouseClient = @import("../src/main.zig").ClickHouseClient;
-const ClickHouseConfig = @import("../src/main.zig").ClickHouseConfig;
-const Transaction = @import("../src/transaction.zig").Transaction;
-
-pub fn main() !void {
- var gpa = std.heap.GeneralPurposeAllocator(.{}){};
- defer _ = gpa.deinit();
- const allocator = gpa.allocator();
-
- const config = ClickHouseConfig{
- .host = "localhost",
- .port = 9000,
- .username = "default",
- .password = "",
- .database = "default",
- };
-
- var client = ClickHouseClient.init(allocator, config);
- defer client.deinit();
-
- try client.connect();
-
- // Start transaction
- var tx = try Transaction.begin(&client, allocator);
- defer tx.deinit();
-
- // Execute queries within transaction
- tx.query(
- \\INSERT INTO test_table (id, name)
- \\VALUES (1, 'test1')
- ) catch |err| {
- try tx.rollback();
- return err;
- };
-
- tx.query(
- \\INSERT INTO test_table (id, name)
- \\VALUES (2, 'test2')
- ) catch |err| {
- try tx.rollback();
- return err;
- };
-
- // Commit transaction
- try tx.commit();
-
- std.debug.print("Transaction completed successfully\n", .{});
-}
\ No newline at end of file
diff --git a/experimental/legacy_src/async_client.zig b/experimental/legacy_src/async_client.zig
deleted file mode 100644
index e296698..0000000
--- a/experimental/legacy_src/async_client.zig
+++ /dev/null
@@ -1,122 +0,0 @@
-const std = @import("std");
-const net = std.net;
-const ClickHouseConfig = @import("main.zig").ClickHouseConfig;
-const protocol = @import("protocol.zig");
-const packet = @import("packet.zig");
-const block = @import("block.zig");
-const error = @import("error.zig");
-const tls = @import("tls.zig");
-
-pub const AsyncClient = struct {
- config: ClickHouseConfig,
- stream: ?net.Stream,
- tls_ctx: ?*tls.TlsContext,
- allocator: std.mem.Allocator,
- frame: @Frame(queryAsync),
-
- pub fn init(allocator: std.mem.Allocator, config: ClickHouseConfig) AsyncClient {
- return .{
- .config = config,
- .stream = null,
- .tls_ctx = null,
- .allocator = allocator,
- .frame = undefined,
- };
- }
-
- pub fn deinit(self: *AsyncClient) void {
- if (self.tls_ctx) |ctx| {
- ctx.deinit();
- }
- if (self.stream) |stream| {
- stream.close();
- }
- }
-
- pub fn connect(self: *AsyncClient) !void {
- const address = try net.Address.parseIp(self.config.host, self.config.port);
- self.stream = try net.tcpConnectToAddress(address);
-
- if (self.config.tls_config) |tls_config| {
- self.tls_ctx = try tls.TlsContext.init(self.allocator, tls_config);
- try self.tls_ctx.?.connect(self.stream.?);
- }
-
- try self.sendHello();
- try self.readServerHello();
- }
-
- pub fn queryAsync(self: *AsyncClient, query_str: []const u8) !void {
- if (self.stream == null) {
- return error.ConnectionFailed;
- }
-
- var writer = if (self.tls_ctx) |ctx| ctx.writer() else self.stream.?.writer();
-
- try packet.writePacketHeader(writer, .Query);
- try protocol.ClientInfo.write(writer, "", "ClickHouse Zig Async");
- try self.config.settings.write(writer);
-
- try writer.writeIntLittle(u64, query_str.len);
- try writer.writeAll(query_str);
-
- try self.processQueryResponseAsync();
- }
-
- fn processQueryResponseAsync(self: *AsyncClient) !void {
- var reader = if (self.tls_ctx) |ctx| ctx.reader() else self.stream.?.reader();
-
- while (true) {
- const packet_type = try reader.readIntLittle(u64);
-
- switch (@as(packet.PacketType, @enumFromInt(packet_type))) {
- .Data => {
- suspend {
- // Process data asynchronously
- resume self.frame;
- }
- },
- .Progress => {
- suspend {
- // Process progress asynchronously
- resume self.frame;
- }
- },
- .EndOfStream => return,
- .Error => {
- const err = try error.Error.readFromServer(reader, self.allocator);
- defer err.deinit();
- return error.QueryFailed;
- },
- else => {},
- }
- }
- }
-
- fn sendHello(self: *AsyncClient) !void {
- var writer = if (self.tls_ctx) |ctx| ctx.writer() else self.stream.?.writer();
-
- try packet.writePacketHeader(writer, .Hello);
- try protocol.ClientHello.write(writer);
-
- try writer.writeIntLittle(u8, @as(u8, @truncate(self.config.database.len)));
- try writer.writeAll(self.config.database);
-
- try writer.writeIntLittle(u8, @as(u8, @truncate(self.config.username.len)));
- try writer.writeAll(self.config.username);
-
- try writer.writeIntLittle(u8, @as(u8, @truncate(self.config.password.len)));
- try writer.writeAll(self.config.password);
- }
-
- fn readServerHello(self: *AsyncClient) !void {
- var reader = if (self.tls_ctx) |ctx| ctx.reader() else self.stream.?.reader();
-
- const server_packet = try reader.readIntLittle(u64);
- if (server_packet != @intFromEnum(packet.PacketType.Hello)) {
- return error.ProtocolError;
- }
-
- _ = try protocol.ServerHello.read(reader);
- }
-};
\ No newline at end of file
diff --git a/experimental/legacy_src/async_query.zig b/experimental/legacy_src/async_query.zig
deleted file mode 100644
index 892cbb0..0000000
--- a/experimental/legacy_src/async_query.zig
+++ /dev/null
@@ -1,111 +0,0 @@
-const std = @import("std");
-const AsyncClient = @import("async_client.zig").AsyncClient;
-const results = @import("results.zig");
-const block = @import("block.zig");
-const error = @import("error.zig");
-
-pub const AsyncQuery = struct {
- client: *AsyncClient,
- query: []const u8,
- allocator: std.mem.Allocator,
- current_block: ?*block.Block,
- frame: @Frame(execute),
- callback: ?QueryCallback,
-
- pub const QueryCallback = fn (*AsyncQuery, ?*results.QueryResult, ?*error.Error) void;
-
- pub fn init(allocator: std.mem.Allocator, client: *AsyncClient, query: []const u8) AsyncQuery {
- return .{
- .client = client,
- .query = allocator.dupe(u8, query) catch unreachable,
- .allocator = allocator,
- .current_block = null,
- .frame = undefined,
- .callback = null,
- };
- }
-
- pub fn deinit(self: *AsyncQuery) void {
- if (self.current_block) |b| {
- b.deinit();
- self.allocator.destroy(b);
- }
- self.allocator.free(self.query);
- }
-
- pub fn execute(self: *AsyncQuery) !void {
- if (self.client.stream == null) {
- return error.ConnectionFailed;
- }
-
- self.frame = async self.executeInternal();
- try await self.frame;
- }
-
- pub fn executeWithCallback(self: *AsyncQuery, callback: QueryCallback) !void {
- self.callback = callback;
- try self.execute();
- }
-
- fn executeInternal(self: *AsyncQuery) !void {
- // Start query execution
- try self.client.queryAsync(self.query);
-
- // Process results asynchronously
- var result: ?*results.QueryResult = null;
- var err: ?*error.Error = null;
-
- if (self.current_block) |b| {
- result = try results.QueryResult.init(self.allocator, b);
- }
-
- // Call callback if set
- if (self.callback) |cb| {
- cb(self, result, err);
- }
- }
-
- pub fn cancel(self: *AsyncQuery) void {
- // Cancel the ongoing query
- if (self.frame) |frame| {
- frame.cancel();
- }
- }
-
- pub const AsyncIterator = struct {
- query: *AsyncQuery,
- current_row: usize,
-
- pub fn next(self: *AsyncIterator) !?results.Row {
- if (self.query.current_block == null or
- self.current_row >= self.query.current_block.?.rows) {
- // Fetch next block asynchronously
- if (!try self.query.fetchNextBlockAsync()) {
- return null;
- }
- self.current_row = 0;
- }
-
- const row = try results.Row.fromBlock(
- self.query.allocator,
- self.query.current_block.?,
- self.current_row
- );
- self.current_row += 1;
- return row;
- }
- };
-
- pub fn iterator(self: *AsyncQuery) AsyncIterator {
- return .{
- .query = self,
- .current_row = 0,
- };
- }
-
- fn fetchNextBlockAsync(self: *AsyncQuery) !bool {
- // This would be implemented to fetch the next block asynchronously
- // For now, return false to indicate no more blocks
- return false;
- }
-};
\ No newline at end of file
diff --git a/experimental/legacy_src/block.zig b/experimental/legacy_src/block.zig
deleted file mode 100644
index 6f910e0..0000000
--- a/experimental/legacy_src/block.zig
+++ /dev/null
@@ -1,49 +0,0 @@
-const std = @import("std");
-const Allocator = std.mem.Allocator;
-const types = @import("types.zig");
-
-pub const Column = struct {
- name: []const u8,
- type: types.ClickHouseType,
- data: []const u8,
- is_nullable: bool,
-
- pub fn init(allocator: Allocator, name: []const u8, type_str: []const u8) !Column {
- const ch_type = try types.ClickHouseType.fromStr(type_str);
- return Column{
- .name = try allocator.dupe(u8, name),
- .type = ch_type,
- .data = &[_]u8{},
- .is_nullable = std.mem.startsWith(u8, type_str, "Nullable"),
- };
- }
-};
-
-pub const Block = struct {
- columns: []Column,
- rows: usize,
- allocator: Allocator,
-
- pub fn init(allocator: Allocator) Block {
- return Block{
- .columns = &[_]Column{},
- .rows = 0,
- .allocator = allocator,
- };
- }
-
- pub fn addColumn(self: *Block, name: []const u8, type_str: []const u8) !void {
- const column = try Column.init(self.allocator, name, type_str);
- const new_columns = try self.allocator.realloc(self.columns, self.columns.len + 1);
- new_columns[new_columns.len - 1] = column;
- self.columns = new_columns;
- }
-
- pub fn deinit(self: *Block) void {
- for (self.columns) |column| {
- self.allocator.free(column.name);
- self.allocator.free(column.data);
- }
- self.allocator.free(self.columns);
- }
-};
\ No newline at end of file
diff --git a/experimental/legacy_src/bulk_insert.zig b/experimental/legacy_src/bulk_insert.zig
deleted file mode 100644
index 7f13d7d..0000000
--- a/experimental/legacy_src/bulk_insert.zig
+++ /dev/null
@@ -1,167 +0,0 @@
-const std = @import("std");
-const block = @import("block.zig");
-const types = @import("types.zig");
-const compression = @import("compression.zig");
-
-pub const BulkInsert = struct {
- allocator: std.mem.Allocator,
- table: []const u8,
- columns: []Column,
- batch_size: usize,
- current_row: usize,
- compression_method: compression.CompressionMethod,
-
- pub const Column = struct {
- name: []const u8,
- type_info: types.TypeInfo,
- data: std.ArrayList(u8),
- };
-
- pub fn init(
- allocator: std.mem.Allocator,
- table: []const u8,
- column_defs: []const ColumnDef,
- batch_size: usize,
- ) !BulkInsert {
- var columns = try allocator.alloc(Column, column_defs.len);
-
- for (column_defs, 0..) |def, i| {
- columns[i] = .{
- .name = try allocator.dupe(u8, def.name),
- .type_info = try types.TypeInfo.parse(allocator, def.type_str),
- .data = std.ArrayList(u8).init(allocator),
- };
- }
-
- return BulkInsert{
- .allocator = allocator,
- .table = try allocator.dupe(u8, table),
- .columns = columns,
- .batch_size = batch_size,
- .current_row = 0,
- .compression_method = .None,
- };
- }
-
- pub fn deinit(self: *BulkInsert) void {
- for (self.columns) |*column| {
- self.allocator.free(column.name);
- column.data.deinit();
- }
- self.allocator.free(self.columns);
- self.allocator.free(self.table);
- }
-
- pub fn setCompression(self: *BulkInsert, method: compression.CompressionMethod) void {
- self.compression_method = method;
- }
-
- pub fn addRow(self: *BulkInsert, values: []const Value) !bool {
- if (values.len != self.columns.len) return error.ColumnCountMismatch;
-
- for (values, 0..) |value, i| {
- try self.addValue(&self.columns[i], value);
- }
-
- self.current_row += 1;
- return self.current_row >= self.batch_size;
- }
-
- pub fn flush(self: *BulkInsert) !void {
- if (self.current_row == 0) return;
-
- var block_data = try self.createBlock();
- defer block_data.deinit();
-
- if (self.compression_method != .None) {
- const compressed = try compression.CompressedData.compress(
- self.allocator,
- block_data.data,
- self.compression_method,
- );
- defer compressed.deinit(self.allocator);
- // Send compressed block
- } else {
- // Send uncompressed block
- }
-
- self.reset();
- }
-
- fn addValue(self: *BulkInsert, column: *Column, value: Value) !void {
- switch (value) {
- .Int8 => |v| try column.data.writer().writeIntLittle(i8, v),
- .Int16 => |v| try column.data.writer().writeIntLittle(i16, v),
- .Int32 => |v| try column.data.writer().writeIntLittle(i32, v),
- .Int64 => |v| try column.data.writer().writeIntLittle(i64, v),
- .UInt8 => |v| try column.data.writer().writeIntLittle(u8, v),
- .UInt16 => |v| try column.data.writer().writeIntLittle(u16, v),
- .UInt32 => |v| try column.data.writer().writeIntLittle(u32, v),
- .UInt64 => |v| try column.data.writer().writeIntLittle(u64, v),
- .Float32 => |v| try column.data.writer().writeIntLittle(f32, v),
- .Float64 => |v| try column.data.writer().writeIntLittle(f64, v),
- .String => |v| {
- try column.data.writer().writeIntLittle(u64, v.len);
- try column.data.appendSlice(v);
- },
- .DateTime => |v| try column.data.writer().writeIntLittle(i64, v),
- .Date => |v| try column.data.writer().writeIntLittle(i32, v),
- .UUID => |v| try column.data.appendSlice(&v),
- .Array => |v| try self.addArray(column, v),
- .Null => if (column.type_info.nullable) {
- try column.data.append(1);
- } else return error.NullValueForNonNullableColumn,
- }
- }
-
- fn addArray(self: *BulkInsert, column: *Column, values: []const Value) !void {
- try column.data.writer().writeIntLittle(u64, values.len);
- for (values) |value| {
- try self.addValue(column, value);
- }
- }
-
- fn createBlock(self: *BulkInsert) !block.Block {
- var result = block.Block.init(self.allocator);
-
- for (self.columns) |column| {
- try result.addColumn(column.name, @tagName(column.type_info.base_type));
- const col_idx = result.columns.len - 1;
- result.columns[col_idx].data = try self.allocator.dupe(u8, column.data.items);
- }
-
- result.rows = self.current_row;
- return result;
- }
-
- fn reset(self: *BulkInsert) void {
- for (self.columns) |*column| {
- column.data.clearRetainingCapacity();
- }
- self.current_row = 0;
- }
-};
-
-pub const ColumnDef = struct {
- name: []const u8,
- type_str: []const u8,
-};
-
-pub const Value = union(enum) {
- Int8: i8,
- Int16: i16,
- Int32: i32,
- Int64: i64,
- UInt8: u8,
- UInt16: u16,
- UInt32: u32,
- UInt64: u64,
- Float32: f32,
- Float64: f64,
- String: []const u8,
- DateTime: i64,
- Date: i32,
- UUID: [16]u8,
- Array: []const Value,
- Null,
-};
\ No newline at end of file
diff --git a/experimental/legacy_src/cherror.zig b/experimental/legacy_src/cherror.zig
deleted file mode 100644
index e6f0302..0000000
--- a/experimental/legacy_src/cherror.zig
+++ /dev/null
@@ -1,88 +0,0 @@
-const std = @import("std");
-
-pub const ErrorCode = enum(u32) {
- Unknown = 0,
- NetworkError = 1,
- BadResponse = 2,
- ParseError = 3,
- TypeMismatch = 4,
- Timeout = 5,
- Cancelled = 6,
- ServerError = 7,
- ProtocolError = 8,
- CompressionError = 9,
-
- pub fn fromInt(code: u32) ErrorCode {
- return std.meta.intToEnum(ErrorCode, code) catch .Unknown;
- }
-};
-
-pub const Error = struct {
- code: ErrorCode,
- message: []const u8,
- stack_trace: ?[]const u8,
- nested: ?*Error,
- allocator: std.mem.Allocator,
-
- pub fn init(allocator: std.mem.Allocator, code: ErrorCode, message: []const u8) !*Error {
- var err = try allocator.create(Error);
- err.* = .{
- .code = code,
- .message = try allocator.dupe(u8, message),
- .stack_trace = null,
- .nested = null,
- .allocator = allocator,
- };
- return err;
- }
-
- pub fn initWithStack(allocator: std.mem.Allocator, code: ErrorCode, message: []const u8, stack_trace: []const u8) !*Error {
- var err = try allocator.create(Error);
- err.* = .{
- .code = code,
- .message = try allocator.dupe(u8, message),
- .stack_trace = try allocator.dupe(u8, stack_trace),
- .nested = null,
- .allocator = allocator,
- };
- return err;
- }
-
- pub fn wrap(allocator: std.mem.Allocator, code: ErrorCode, message: []const u8, nested: *Error) !*Error {
- var err = try allocator.create(Error);
- err.* = .{
- .code = code,
- .message = try allocator.dupe(u8, message),
- .stack_trace = null,
- .nested = nested,
- .allocator = allocator,
- };
- return err;
- }
-
- pub fn deinit(self: *Error) void {
- self.allocator.free(self.message);
- if (self.stack_trace) |st| {
- self.allocator.free(st);
- }
- if (self.nested) |nested| {
- nested.deinit();
- self.allocator.destroy(nested);
- }
- self.allocator.destroy(self);
- }
-
- pub fn format(self: Error, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void {
- _ = fmt;
- _ = options;
-
- try writer.print("{s}: {s}", .{ @tagName(self.code), self.message });
- if (self.stack_trace) |st| {
- try writer.print("\nStack trace:\n{s}", .{st});
- }
- if (self.nested) |nested| {
- try writer.print("\nCaused by: ", .{});
- try nested.format(fmt, options, writer);
- }
- }
-};
\ No newline at end of file
diff --git a/experimental/legacy_src/complex_types.zig b/experimental/legacy_src/complex_types.zig
deleted file mode 100644
index a45fec0..0000000
--- a/experimental/legacy_src/complex_types.zig
+++ /dev/null
@@ -1,197 +0,0 @@
-const std = @import("std");
-const types = @import("types.zig");
-
-pub const LowCardinality = struct {
- base_type: types.TypeInfo,
- dictionary: []DictionaryEntry,
- dictionary_size: usize,
-
- pub const DictionaryEntry = struct {
- key: u32,
- value: Value,
- };
-
- pub const Value = union(enum) {
- String: []const u8,
- Int8: i8,
- Int16: i16,
- Int32: i32,
- Int64: i64,
- UInt8: u8,
- UInt16: u16,
- UInt32: u32,
- UInt64: u64,
- Float32: f32,
- Float64: f64,
- };
-
- pub fn init(base_type: types.TypeInfo) LowCardinality {
- return .{
- .base_type = base_type,
- .dictionary = &[_]DictionaryEntry{},
- .dictionary_size = 0,
- };
- }
-
- pub fn deinit(self: *LowCardinality, allocator: std.mem.Allocator) void {
- for (self.dictionary) |entry| {
- switch (entry.value) {
- .String => |s| allocator.free(s),
- else => {},
- }
- }
- allocator.free(self.dictionary);
- }
-};
-
-pub const AggregateFunction = struct {
- name: []const u8,
- argument_types: []types.TypeInfo,
- return_type: types.TypeInfo,
-
- pub fn init(
- allocator: std.mem.Allocator,
- name: []const u8,
- argument_types: []const types.TypeInfo,
- return_type: types.TypeInfo,
- ) !AggregateFunction {
- return AggregateFunction{
- .name = try allocator.dupe(u8, name),
- .argument_types = try allocator.dupe(types.TypeInfo, argument_types),
- .return_type = return_type,
- };
- }
-
- pub fn deinit(self: *AggregateFunction, allocator: std.mem.Allocator) void {
- allocator.free(self.name);
- allocator.free(self.argument_types);
- }
-};
-
-pub const SimpleAggregateFunction = struct {
- name: []const u8,
- argument_type: types.TypeInfo,
-
- pub fn init(allocator: std.mem.Allocator, name: []const u8, argument_type: types.TypeInfo) !SimpleAggregateFunction {
- return SimpleAggregateFunction{
- .name = try allocator.dupe(u8, name),
- .argument_type = argument_type,
- };
- }
-
- pub fn deinit(self: *SimpleAggregateFunction, allocator: std.mem.Allocator) void {
- allocator.free(self.name);
- }
-};
-
-pub const Nested = struct {
- name: []const u8,
- types: []types.TypeInfo,
- nullable: bool,
-
- pub fn init(allocator: std.mem.Allocator, name: []const u8, field_types: []const types.TypeInfo) !Nested {
- var types_copy = try allocator.alloc(types.TypeInfo, field_types.len);
- @memcpy(types_copy, field_types);
-
- return Nested{
- .name = try allocator.dupe(u8, name),
- .types = types_copy,
- .nullable = false,
- };
- }
-
- pub fn deinit(self: *Nested, allocator: std.mem.Allocator) void {
- allocator.free(self.name);
- allocator.free(self.types);
- }
-};
-
-pub const Map = struct {
- key_type: types.TypeInfo,
- value_type: types.TypeInfo,
- nullable: bool,
-
- pub fn init(key_type: types.TypeInfo, value_type: types.TypeInfo) Map {
- return .{
- .key_type = key_type,
- .value_type = value_type,
- .nullable = false,
- };
- }
-};
-
-pub const Point = struct {
- x: f64,
- y: f64,
-};
-
-pub const Ring = struct {
- points: []Point,
-
- pub fn init(allocator: std.mem.Allocator, points: []const Point) !Ring {
- return Ring{
- .points = try allocator.dupe(Point, points),
- };
- }
-
- pub fn deinit(self: *Ring, allocator: std.mem.Allocator) void {
- allocator.free(self.points);
- }
-};
-
-pub const Polygon = struct {
- rings: []Ring,
-
- pub fn init(allocator: std.mem.Allocator, rings: []const Ring) !Polygon {
- return Polygon{
- .rings = try allocator.dupe(Ring, rings),
- };
- }
-
- pub fn deinit(self: *Polygon, allocator: std.mem.Allocator) void {
- for (self.rings) |*ring| {
- ring.deinit(allocator);
- }
- allocator.free(self.rings);
- }
-};
-
-pub const MultiPolygon = struct {
- polygons: []Polygon,
-
- pub fn init(allocator: std.mem.Allocator, polygons: []const Polygon) !MultiPolygon {
- return MultiPolygon{
- .polygons = try allocator.dupe(Polygon, polygons),
- };
- }
-
- pub fn deinit(self: *MultiPolygon, allocator: std.mem.Allocator) void {
- for (self.polygons) |*polygon| {
- polygon.deinit(allocator);
- }
- allocator.free(self.polygons);
- }
-};
-
-pub const IPv4 = struct {
- octets: [4]u8,
-};
-
-pub const IPv6 = struct {
- segments: [8]u16,
-};
-
-pub const JSON = struct {
- value: std.json.Value,
-
- pub fn init(allocator: std.mem.Allocator, json_str: []const u8) !JSON {
- const parsed = try std.json.parseFromSlice(std.json.Value, allocator, json_str, .{});
- return JSON{
- .value = parsed.value,
- };
- }
-
- pub fn deinit(self: *JSON, allocator: std.mem.Allocator) void {
- self.value.deinit(allocator);
- }
-};
\ No newline at end of file
diff --git a/experimental/legacy_src/compression.zig b/experimental/legacy_src/compression.zig
deleted file mode 100644
index 027b473..0000000
--- a/experimental/legacy_src/compression.zig
+++ /dev/null
@@ -1,30 +0,0 @@
-// Compression stub for v0.16.0 sync release.
-// LZ4/ZSTD implementations live in experimental/compression/ pending
-// Zig 0.16 port and proper dependency packaging.
-
-const std = @import("std");
-
-pub const CompressionMethod = enum(u8) {
- None = 0x02,
- LZ4 = 0x82,
- ZSTD = 0x90,
-};
-
-pub const CompressedData = struct {
- data: []u8,
-
- pub fn compress(
- allocator: std.mem.Allocator,
- input: []const u8,
- method: CompressionMethod,
- ) !CompressedData {
- _ = method;
- const copy = try allocator.alloc(u8, input.len);
- @memcpy(copy, input);
- return .{ .data = copy };
- }
-
- pub fn deinit(self: CompressedData, allocator: std.mem.Allocator) void {
- allocator.free(self.data);
- }
-};
diff --git a/experimental/legacy_src/compression/lz4.zig b/experimental/legacy_src/compression/lz4.zig
deleted file mode 100644
index 28e23d7..0000000
--- a/experimental/legacy_src/compression/lz4.zig
+++ /dev/null
@@ -1,45 +0,0 @@
-const std = @import("std");
-const c = @cImport({
- @cInclude("lz4.h");
-});
-
-pub fn compress(allocator: std.mem.Allocator, data: []const u8) ![]u8 {
- const max_dst_size = c.LZ4_compressBound(@intCast(c_int, data.len));
- var compressed = try allocator.alloc(u8, @intCast(usize, max_dst_size));
- errdefer allocator.free(compressed);
-
- const compressed_size = c.LZ4_compress_default(
- @ptrCast([*c]const u8, data.ptr),
- @ptrCast([*c]u8, compressed.ptr),
- @intCast(c_int, data.len),
- @intCast(c_int, compressed.len),
- );
-
- if (compressed_size <= 0) {
- return error.CompressionFailed;
- }
-
- return allocator.realloc(compressed, @intCast(usize, compressed_size));
-}
-
-pub fn decompress(allocator: std.mem.Allocator, compressed: []const u8, original_size: usize) ![]u8 {
- var decompressed = try allocator.alloc(u8, original_size);
- errdefer allocator.free(decompressed);
-
- const decompressed_size = c.LZ4_decompress_safe(
- @ptrCast([*c]const u8, compressed.ptr),
- @ptrCast([*c]u8, decompressed.ptr),
- @intCast(c_int, compressed.len),
- @intCast(c_int, decompressed.len),
- );
-
- if (decompressed_size < 0) {
- return error.DecompressionFailed;
- }
-
- if (decompressed_size != original_size) {
- return error.SizeMismatch;
- }
-
- return decompressed;
-}
\ No newline at end of file
diff --git a/experimental/legacy_src/compression/zstd.zig b/experimental/legacy_src/compression/zstd.zig
deleted file mode 100644
index c08b997..0000000
--- a/experimental/legacy_src/compression/zstd.zig
+++ /dev/null
@@ -1,46 +0,0 @@
-const std = @import("std");
-const c = @cImport({
- @cInclude("zstd.h");
-});
-
-pub fn compress(allocator: std.mem.Allocator, data: []const u8, level: c_int) ![]u8 {
- const max_dst_size = c.ZSTD_compressBound(data.len);
- var compressed = try allocator.alloc(u8, max_dst_size);
- errdefer allocator.free(compressed);
-
- const compressed_size = c.ZSTD_compress(
- compressed.ptr,
- compressed.len,
- data.ptr,
- data.len,
- level,
- );
-
- if (c.ZSTD_isError(compressed_size) != 0) {
- return error.CompressionFailed;
- }
-
- return allocator.realloc(compressed, compressed_size);
-}
-
-pub fn decompress(allocator: std.mem.Allocator, compressed: []const u8, original_size: usize) ![]u8 {
- var decompressed = try allocator.alloc(u8, original_size);
- errdefer allocator.free(decompressed);
-
- const decompressed_size = c.ZSTD_decompress(
- decompressed.ptr,
- decompressed.len,
- compressed.ptr,
- compressed.len,
- );
-
- if (c.ZSTD_isError(decompressed_size) != 0) {
- return error.DecompressionFailed;
- }
-
- if (decompressed_size != original_size) {
- return error.SizeMismatch;
- }
-
- return decompressed;
-}
\ No newline at end of file
diff --git a/experimental/legacy_src/ddl.zig b/experimental/legacy_src/ddl.zig
deleted file mode 100644
index eb6871b..0000000
--- a/experimental/legacy_src/ddl.zig
+++ /dev/null
@@ -1,220 +0,0 @@
-const std = @import("std");
-const ClickHouseClient = @import("main.zig").ClickHouseClient;
-
-pub const MaterializedView = struct {
- name: []const u8,
- target_table: []const u8,
- query: []const u8,
- allocator: std.mem.Allocator,
-
- pub fn init(allocator: std.mem.Allocator, name: []const u8, target_table: []const u8, query: []const u8) !MaterializedView {
- return MaterializedView{
- .name = try allocator.dupe(u8, name),
- .target_table = try allocator.dupe(u8, target_table),
- .query = try allocator.dupe(u8, query),
- .allocator = allocator,
- };
- }
-
- pub fn deinit(self: *MaterializedView) void {
- self.allocator.free(self.name);
- self.allocator.free(self.target_table);
- self.allocator.free(self.query);
- }
-
- pub fn create(self: *MaterializedView, client: *ClickHouseClient) !void {
- var query_buf = std.ArrayList(u8).init(self.allocator);
- defer query_buf.deinit();
-
- try query_buf.writer().print(
- \\CREATE MATERIALIZED VIEW {s}
- \\TO {s}
- \\AS {s}
- , .{ self.name, self.target_table, self.query });
-
- try client.query(query_buf.items);
- }
-
- pub fn drop(self: *MaterializedView, client: *ClickHouseClient) !void {
- var query_buf = std.ArrayList(u8).init(self.allocator);
- defer query_buf.deinit();
-
- try query_buf.writer().print("DROP VIEW IF EXISTS {s}", .{self.name});
- try client.query(query_buf.items);
- }
-};
-
-pub const Mutation = struct {
- table: []const u8,
- mutation: []const u8,
- allocator: std.mem.Allocator,
- settings: MutationSettings,
-
- pub const MutationSettings = struct {
- replication: bool = true,
- mutations_sync: bool = false,
- };
-
- pub fn init(allocator: std.mem.Allocator, table: []const u8, mutation: []const u8) Mutation {
- return .{
- .table = allocator.dupe(u8, table) catch unreachable,
- .mutation = allocator.dupe(u8, mutation) catch unreachable,
- .allocator = allocator,
- .settings = .{},
- };
- }
-
- pub fn deinit(self: *Mutation) void {
- self.allocator.free(self.table);
- self.allocator.free(self.mutation);
- }
-
- pub fn execute(self: *Mutation, client: *ClickHouseClient) !void {
- var query_buf = std.ArrayList(u8).init(self.allocator);
- defer query_buf.deinit();
-
- // Build ALTER TABLE query with mutation
- try query_buf.writer().print("ALTER TABLE {s} {s}", .{ self.table, self.mutation });
-
- // Add settings if needed
- if (!self.settings.replication) {
- try query_buf.appendSlice(" SETTINGS replication_alter_partitions_sync = 0");
- }
- if (self.settings.mutations_sync) {
- try query_buf.appendSlice(" SETTINGS mutations_sync = 1");
- }
-
- try client.query(query_buf.items);
- }
-
- pub fn status(self: *Mutation, client: *ClickHouseClient) !MutationStatus {
- var query_buf = std.ArrayList(u8).init(self.allocator);
- defer query_buf.deinit();
-
- try query_buf.writer().print(
- \\SELECT
- \\ is_done,
- \\ latest_failed_part,
- \\ latest_fail_reason,
- \\ parts_to_do_count
- \\FROM system.mutations
- \\WHERE table = '{s}'
- \\ORDER BY create_time DESC
- \\LIMIT 1
- , .{self.table});
-
- try client.query(query_buf.items);
- // Process result and return status
- return MutationStatus{
- .is_done = false,
- .parts_to_do = 0,
- .failed_part = null,
- .fail_reason = null,
- };
- }
-};
-
-pub const MutationStatus = struct {
- is_done: bool,
- parts_to_do: u64,
- failed_part: ?[]const u8,
- fail_reason: ?[]const u8,
-};
-
-pub const TableOptions = struct {
- engine: Engine = .MergeTree,
- order_by: ?[]const u8 = null,
- partition_by: ?[]const u8 = null,
- primary_key: ?[]const u8 = null,
- sample_by: ?[]const u8 = null,
- settings: TableSettings = .{},
-
- pub const Engine = enum {
- MergeTree,
- ReplacingMergeTree,
- SummingMergeTree,
- AggregatingMergeTree,
- CollapsingMergeTree,
- VersionedCollapsingMergeTree,
- GraphiteMergeTree,
- Memory,
- Buffer,
- };
-
- pub const TableSettings = struct {
- index_granularity: u64 = 8192,
- enable_mixed_granularity_parts: bool = true,
- min_merge_bytes_to_use_direct_io: u64 = 10 * 1024 * 1024,
- merge_with_ttl_timeout: u64 = 3600,
- };
-};
-
-pub fn createTable(
- client: *ClickHouseClient,
- allocator: std.mem.Allocator,
- name: []const u8,
- columns: []const Column,
- options: TableOptions,
-) !void {
- var query_buf = std.ArrayList(u8).init(allocator);
- defer query_buf.deinit();
-
- // Start CREATE TABLE query
- try query_buf.writer().print("CREATE TABLE {s} (\n", .{name});
-
- // Add columns
- for (columns, 0..) |col, i| {
- if (i > 0) try query_buf.appendSlice(",\n");
- try query_buf.writer().print(" {s} {s}", .{ col.name, col.type });
- if (col.codec) |codec| {
- try query_buf.writer().print(" CODEC({s})", .{codec});
- }
- if (col.ttl) |ttl| {
- try query_buf.writer().print(" TTL {s}", .{ttl});
- }
- }
-
- // Add engine and options
- try query_buf.appendSlice("\n) ENGINE = ");
- try query_buf.appendSlice(@tagName(options.engine));
-
- if (options.order_by) |order| {
- try query_buf.writer().print("\nORDER BY {s}", .{order});
- }
-
- if (options.partition_by) |partition| {
- try query_buf.writer().print("\nPARTITION BY {s}", .{partition});
- }
-
- if (options.primary_key) |pk| {
- try query_buf.writer().print("\nPRIMARY KEY {s}", .{pk});
- }
-
- if (options.sample_by) |sample| {
- try query_buf.writer().print("\nSAMPLE BY {s}", .{sample});
- }
-
- // Add settings
- try query_buf.appendSlice("\nSETTINGS");
- try query_buf.writer().print(
- \\
- \\ index_granularity = {d},
- \\ enable_mixed_granularity_parts = {d},
- \\ min_merge_bytes_to_use_direct_io = {d},
- \\ merge_with_ttl_timeout = {d}
- , .{
- options.settings.index_granularity,
- @boolToInt(options.settings.enable_mixed_granularity_parts),
- options.settings.min_merge_bytes_to_use_direct_io,
- options.settings.merge_with_ttl_timeout,
- });
-
- try client.query(query_buf.items);
-}
-
-pub const Column = struct {
- name: []const u8,
- type: []const u8,
- codec: ?[]const u8 = null,
- ttl: ?[]const u8 = null,
-};
\ No newline at end of file
diff --git a/experimental/legacy_src/dictionary.zig b/experimental/legacy_src/dictionary.zig
deleted file mode 100644
index 1da6f29..0000000
--- a/experimental/legacy_src/dictionary.zig
+++ /dev/null
@@ -1,220 +0,0 @@
-const std = @import("std");
-const ClickHouseClient = @import("main.zig").ClickHouseClient;
-
-pub const Dictionary = struct {
- name: []const u8,
- source: DictionarySource,
- layout: DictionaryLayout,
- lifetime: DictionaryLifetime,
- structure: []const DictionaryAttribute,
- allocator: std.mem.Allocator,
-
- pub const DictionarySource = union(enum) {
- ClickHouse: struct {
- db: []const u8,
- table: []const u8,
- },
- File: struct {
- path: []const u8,
- format: FileFormat,
- },
- MySQL: struct {
- host: []const u8,
- port: u16,
- db: []const u8,
- table: []const u8,
- user: []const u8,
- password: []const u8,
- },
- MongoDB: struct {
- uri: []const u8,
- db: []const u8,
- collection: []const u8,
- },
-
- pub const FileFormat = enum {
- CSV,
- TSV,
- JSONEachRow,
- Parquet,
- };
- };
-
- pub const DictionaryLayout = union(enum) {
- FlatArray,
- HashTable,
- ComplexKeyHashTable,
- DirectArray,
- IPTrie,
- RangeHashed: struct {
- range_min: []const u8,
- range_max: []const u8,
- },
- };
-
- pub const DictionaryLifetime = union(enum) {
- Default,
- TTL: u64,
- KeepAlive: struct {
- min_sec: u64,
- max_sec: u64,
- },
- };
-
- pub const DictionaryAttribute = struct {
- name: []const u8,
- type: []const u8,
- expression: ?[]const u8 = null,
- hierarchical: bool = false,
- injective: bool = false,
- };
-
- pub fn init(
- allocator: std.mem.Allocator,
- name: []const u8,
- source: DictionarySource,
- layout: DictionaryLayout,
- lifetime: DictionaryLifetime,
- structure: []const DictionaryAttribute,
- ) !*Dictionary {
- var dict = try allocator.create(Dictionary);
- dict.* = .{
- .name = try allocator.dupe(u8, name),
- .source = source,
- .layout = layout,
- .lifetime = lifetime,
- .structure = try allocator.dupe(DictionaryAttribute, structure),
- .allocator = allocator,
- };
- return dict;
- }
-
- pub fn deinit(self: *Dictionary) void {
- self.allocator.free(self.name);
- switch (self.source) {
- .ClickHouse => |ch| {
- self.allocator.free(ch.db);
- self.allocator.free(ch.table);
- },
- .File => |f| {
- self.allocator.free(f.path);
- },
- .MySQL => |mysql| {
- self.allocator.free(mysql.host);
- self.allocator.free(mysql.db);
- self.allocator.free(mysql.table);
- self.allocator.free(mysql.user);
- self.allocator.free(mysql.password);
- },
- .MongoDB => |mongo| {
- self.allocator.free(mongo.uri);
- self.allocator.free(mongo.db);
- self.allocator.free(mongo.collection);
- },
- }
- for (self.structure) |attr| {
- self.allocator.free(attr.name);
- self.allocator.free(attr.type);
- if (attr.expression) |expr| {
- self.allocator.free(expr);
- }
- }
- self.allocator.free(self.structure);
- self.allocator.destroy(self);
- }
-
- pub fn create(self: *Dictionary, client: *ClickHouseClient) !void {
- var query_buf = std.ArrayList(u8).init(self.allocator);
- defer query_buf.deinit();
-
- try query_buf.writer().print("CREATE DICTIONARY {s}\n(\n", .{self.name});
-
- // Add structure
- for (self.structure, 0..) |attr, i| {
- if (i > 0) try query_buf.appendSlice(",\n");
- try query_buf.writer().print(" {s} {s}", .{ attr.name, attr.type });
- if (attr.expression) |expr| {
- try query_buf.writer().print(" EXPRESSION {s}", .{expr});
- }
- if (attr.hierarchical) {
- try query_buf.appendSlice(" HIERARCHICAL");
- }
- if (attr.injective) {
- try query_buf.appendSlice(" INJECTIVE");
- }
- }
-
- // Add source
- try query_buf.appendSlice("\n)\nSOURCE(");
- switch (self.source) {
- .ClickHouse => |ch| try query_buf.writer().print(
- "CLICKHOUSE(DB '{s}' TABLE '{s}')",
- .{ ch.db, ch.table },
- ),
- .File => |f| try query_buf.writer().print(
- "FILE(PATH '{s}' FORMAT {s})",
- .{ f.path, @tagName(f.format) },
- ),
- .MySQL => |mysql| try query_buf.writer().print(
- \\MYSQL(
- \\ HOST '{s}'
- \\ PORT {d}
- \\ DB '{s}'
- \\ TABLE '{s}'
- \\ USER '{s}'
- \\ PASSWORD '{s}'
- \\)
- , .{
- mysql.host,
- mysql.port,
- mysql.db,
- mysql.table,
- mysql.user,
- mysql.password,
- }),
- .MongoDB => |mongo| try query_buf.writer().print(
- \\MONGODB(
- \\ URI '{s}'
- \\ DB '{s}'
- \\ COLLECTION '{s}'
- \\)
- , .{ mongo.uri, mongo.db, mongo.collection }),
- }
-
- // Add layout
- try query_buf.appendSlice(")\nLAYOUT(");
- switch (self.layout) {
- .FlatArray => try query_buf.appendSlice("FLAT"),
- .HashTable => try query_buf.appendSlice("HASHED"),
- .ComplexKeyHashTable => try query_buf.appendSlice("COMPLEX_KEY_HASHED"),
- .DirectArray => try query_buf.appendSlice("DIRECT"),
- .IPTrie => try query_buf.appendSlice("IP_TRIE"),
- .RangeHashed => |range| try query_buf.writer().print(
- "RANGE_HASHED(RANGE_MIN {s} RANGE_MAX {s})",
- .{ range.range_min, range.range_max },
- ),
- }
-
- // Add lifetime
- try query_buf.appendSlice(")\nLIFETIME(");
- switch (self.lifetime) {
- .Default => try query_buf.appendSlice("MIN 300 MAX 360"),
- .TTL => |ttl| try query_buf.writer().print("TTL {d}", .{ttl}),
- .KeepAlive => |ka| try query_buf.writer().print(
- "MIN {d} MAX {d}",
- .{ ka.min_sec, ka.max_sec },
- ),
- }
- try query_buf.appendSlice(")");
-
- try client.query(query_buf.items);
- }
-
- pub fn drop(self: *Dictionary, client: *ClickHouseClient) !void {
- var query_buf = std.ArrayList(u8).init(self.allocator);
- defer query_buf.deinit();
-
- try query_buf.writer().print("DROP DICTIONARY IF EXISTS {s}", .{self.name});
- try client.query(query_buf.items);
- }
-};
\ No newline at end of file
diff --git a/experimental/legacy_src/distributed.zig b/experimental/legacy_src/distributed.zig
deleted file mode 100644
index 8a4569f..0000000
--- a/experimental/legacy_src/distributed.zig
+++ /dev/null
@@ -1,140 +0,0 @@
-const std = @import("std");
-const ClickHouseClient = @import("main.zig").ClickHouseClient;
-
-pub const DistributedTable = struct {
- name: []const u8,
- cluster: []const u8,
- database: []const u8,
- local_table: []const u8,
- sharding_key: ?[]const u8,
- allocator: std.mem.Allocator,
-
- pub fn init(
- allocator: std.mem.Allocator,
- name: []const u8,
- cluster: []const u8,
- database: []const u8,
- local_table: []const u8,
- ) !DistributedTable {
- return DistributedTable{
- .name = try allocator.dupe(u8, name),
- .cluster = try allocator.dupe(u8, cluster),
- .database = try allocator.dupe(u8, database),
- .local_table = try allocator.dupe(u8, local_table),
- .sharding_key = null,
- .allocator = allocator,
- };
- }
-
- pub fn deinit(self: *DistributedTable) void {
- self.allocator.free(self.name);
- self.allocator.free(self.cluster);
- self.allocator.free(self.database);
- self.allocator.free(self.local_table);
- if (self.sharding_key) |key| {
- self.allocator.free(key);
- }
- }
-
- pub fn create(self: *DistributedTable, client: *ClickHouseClient) !void {
- var query_buf = std.ArrayList(u8).init(self.allocator);
- defer query_buf.deinit();
-
- try query_buf.writer().print(
- \\CREATE TABLE {s} AS {s}.{s}
- \\ENGINE = Distributed(
- \\ {s},
- \\ {s},
- \\ {s}
- , .{
- self.name,
- self.database,
- self.local_table,
- self.cluster,
- self.database,
- self.local_table,
- });
-
- if (self.sharding_key) |key| {
- try query_buf.writer().print(",\n {s}", .{key});
- }
-
- try query_buf.appendSlice("\n)");
- try client.query(query_buf.items);
- }
-
- pub fn setShardingKey(self: *DistributedTable, key: []const u8) !void {
- if (self.sharding_key) |old_key| {
- self.allocator.free(old_key);
- }
- self.sharding_key = try self.allocator.dupe(u8, key);
- }
-};
-
-pub const ReplicatedTable = struct {
- name: []const u8,
- zk_path: []const u8,
- replica_name: []const u8,
- engine: Engine,
- allocator: std.mem.Allocator,
-
- pub const Engine = enum {
- ReplicatedMergeTree,
- ReplicatedReplacingMergeTree,
- ReplicatedSummingMergeTree,
- ReplicatedAggregatingMergeTree,
- ReplicatedCollapsingMergeTree,
- ReplicatedVersionedCollapsingMergeTree,
- };
-
- pub fn init(
- allocator: std.mem.Allocator,
- name: []const u8,
- zk_path: []const u8,
- replica_name: []const u8,
- engine: Engine,
- ) !ReplicatedTable {
- return ReplicatedTable{
- .name = try allocator.dupe(u8, name),
- .zk_path = try allocator.dupe(u8, zk_path),
- .replica_name = try allocator.dupe(u8, replica_name),
- .engine = engine,
- .allocator = allocator,
- };
- }
-
- pub fn deinit(self: *ReplicatedTable) void {
- self.allocator.free(self.name);
- self.allocator.free(self.zk_path);
- self.allocator.free(self.replica_name);
- }
-
- pub fn create(self: *ReplicatedTable, client: *ClickHouseClient, columns: []const Column) !void {
- var query_buf = std.ArrayList(u8).init(self.allocator);
- defer query_buf.deinit();
-
- try query_buf.writer().print("CREATE TABLE {s} (\n", .{self.name});
-
- // Add columns
- for (columns, 0..) |col, i| {
- if (i > 0) try query_buf.appendSlice(",\n");
- try query_buf.writer().print(" {s} {s}", .{ col.name, col.type });
- }
-
- // Add engine
- try query_buf.writer().print(
- \\
- \\) ENGINE = {s}(
- \\ '{s}',
- \\ '{s}'
- \\)
- , .{ @tagName(self.engine), self.zk_path, self.replica_name });
-
- try client.query(query_buf.items);
- }
-};
-
-pub const Column = struct {
- name: []const u8,
- type: []const u8,
-};
\ No newline at end of file
diff --git a/experimental/legacy_src/example.zig b/experimental/legacy_src/example.zig
deleted file mode 100644
index c088fc4..0000000
--- a/experimental/legacy_src/example.zig
+++ /dev/null
@@ -1,25 +0,0 @@
-const std = @import("std");
-const ClickHouseClient = @import("main.zig").ClickHouseClient;
-const ClickHouseConfig = @import("main.zig").ClickHouseConfig;
-
-pub fn main() !void {
- var gpa = std.heap.GeneralPurposeAllocator(.{}){};
- defer _ = gpa.deinit();
- const allocator = gpa.allocator();
-
- const config = ClickHouseConfig{
- .host = "localhost",
- .port = 9000,
- .username = "default",
- .password = "",
- .database = "default",
- };
-
- var client = ClickHouseClient.init(allocator, config);
- defer client.deinit();
-
- try client.connect();
- try client.query("SELECT 1");
-
- std.debug.print("Query executed successfully\n", .{});
-}
\ No newline at end of file
diff --git a/experimental/legacy_src/insert.zig b/experimental/legacy_src/insert.zig
deleted file mode 100644
index 6ef60f3..0000000
--- a/experimental/legacy_src/insert.zig
+++ /dev/null
@@ -1,115 +0,0 @@
-const std = @import("std");
-const types = @import("types.zig");
-const block = @import("block.zig");
-
-pub const BulkInsert = struct {
- allocator: std.mem.Allocator,
- table: []const u8,
- columns: []Column,
- batch_size: usize,
- current_row: usize,
-
- pub const Column = struct {
- name: []const u8,
- type_info: types.TypeInfo,
- data: std.ArrayList(u8),
- };
-
- pub fn init(allocator: std.mem.Allocator, table: []const u8, column_defs: []const ColumnDef, batch_size: usize) !BulkInsert {
- var columns = try allocator.alloc(Column, column_defs.len);
-
- for (column_defs, 0..) |def, i| {
- columns[i] = .{
- .name = try allocator.dupe(u8, def.name),
- .type_info = try types.TypeInfo.parse(allocator, def.type_str),
- .data = std.ArrayList(u8).init(allocator),
- };
- }
-
- return BulkInsert{
- .allocator = allocator,
- .table = try allocator.dupe(u8, table),
- .columns = columns,
- .batch_size = batch_size,
- .current_row = 0,
- };
- }
-
- pub fn deinit(self: *BulkInsert) void {
- for (self.columns) |*column| {
- self.allocator.free(column.name);
- column.data.deinit();
- }
- self.allocator.free(self.columns);
- self.allocator.free(self.table);
- }
-
- pub fn addRow(self: *BulkInsert, values: []const Value) !bool {
- if (values.len != self.columns.len) return error.ColumnCountMismatch;
-
- for (values, 0..) |value, i| {
- try self.addValue(&self.columns[i], value);
- }
-
- self.current_row += 1;
- return self.current_row >= self.batch_size;
- }
-
- fn addValue(self: *BulkInsert, column: *Column, value: Value) !void {
- switch (value) {
- .Int8 => |v| try column.data.writer().writeIntLittle(i8, v),
- .Int16 => |v| try column.data.writer().writeIntLittle(i16, v),
- .Int32 => |v| try column.data.writer().writeIntLittle(i32, v),
- .Int64 => |v| try column.data.writer().writeIntLittle(i64, v),
- .UInt8 => |v| try column.data.writer().writeIntLittle(u8, v),
- .UInt16 => |v| try column.data.writer().writeIntLittle(u16, v),
- .UInt32 => |v| try column.data.writer().writeIntLittle(u32, v),
- .UInt64 => |v| try column.data.writer().writeIntLittle(u64, v),
- .Float32 => |v| try column.data.writer().writeIntLittle(f32, v),
- .Float64 => |v| try column.data.writer().writeIntLittle(f64, v),
- .String => |v| {
- try column.data.writer().writeIntLittle(u64, v.len);
- try column.data.appendSlice(v);
- },
- .DateTime => |v| try column.data.writer().writeIntLittle(i64, v),
- .Date => |v| try column.data.writer().writeIntLittle(i32, v),
- .UUID => |v| try column.data.appendSlice(&v),
- .Array => |v| try self.addArray(column, v),
- .Null => if (column.type_info.nullable) {
- try column.data.append(1);
- } else return error.NullValueForNonNullableColumn,
- }
- }
-
- fn addArray(self: *BulkInsert, column: *Column, values: []const Value) !void {
- try column.data.writer().writeIntLittle(u64, values.len);
- for (values) |value| {
- try self.addValue(column, value);
- }
- }
-
- pub fn createBlock(self: *BulkInsert) !block.Block {
- var result = block.Block.init(self.allocator);
-
- for (self.columns) |column| {
- try result.addColumn(column.name, @tagName(column.type_info.base_type));
- const col_idx = result.columns.len - 1;
- result.columns[col_idx].data = try self.allocator.dupe(u8, column.data.items);
- }
-
- result.rows = self.current_row;
- return result;
- }
-
- pub fn reset(self: *BulkInsert) void {
- for (self.columns) |*column| {
- column.data.clearRetainingCapacity();
- }
- self.current_row = 0;
- }
-};
-
-pub const ColumnDef = struct {
- name: []const u8,
- type_str: []const u8,
-};
\ No newline at end of file
diff --git a/experimental/legacy_src/main.zig b/experimental/legacy_src/main.zig
deleted file mode 100644
index e358869..0000000
--- a/experimental/legacy_src/main.zig
+++ /dev/null
@@ -1,206 +0,0 @@
-const std = @import("std");
-const net = std.net;
-const mem = std.mem;
-const protocol = @import("protocol.zig");
-const packet = @import("packet.zig");
-const block = @import("block.zig");
-const settings = @import("settings.zig");
-const compression = @import("compression.zig");
-const results = @import("results.zig");
-const cherror = @import("cherror.zig");
-const server_info = @import("server_info.zig");
-const query_info = @import("query_info.zig");
-const progress = @import("progress.zig");
-const statistics = @import("statistics.zig");
-const profile = @import("profile_info.zig");
-
-pub const ClickHouseError = error{
- ConnectionFailed,
- QueryFailed,
- InvalidResponse,
- OutOfMemory,
- ProtocolError,
- CompressionError,
- TypeMismatch,
- QueryCancelled,
-};
-
-pub const ClickHouseConfig = struct {
- host: []const u8,
- port: u16 = 9000,
- username: []const u8 = "default",
- password: []const u8 = "",
- database: []const u8 = "default",
- settings: settings.Settings = .{},
-};
-
-pub const ClickHouseClient = struct {
- config: ClickHouseConfig,
- stream: ?net.Stream,
- allocator: mem.Allocator,
- current_block: ?block.Block,
- current_result: ?results.QueryResult,
- server_info: ?server_info.ServerInfo,
- query_info: ?query_info.QueryInfo,
- last_error: ?*cherror.Error,
-
- pub fn init(allocator: mem.Allocator, config: ClickHouseConfig) ClickHouseClient {
- return .{
- .config = config,
- .stream = null,
- .allocator = allocator,
- .current_block = null,
- .current_result = null,
- .server_info = null,
- .query_info = null,
- .last_error = null,
- };
- }
-
- pub fn connect(self: *ClickHouseClient) !void {
- const address = try net.Address.parseIp(self.config.host, self.config.port);
- self.stream = try net.tcpConnectToAddress(address);
-
- try self.sendHello();
- try self.readServerHello();
- }
-
- fn sendHello(self: *ClickHouseClient) !void {
- var writer = self.stream.?.writer();
-
- try packet.writePacketHeader(writer, .Hello);
- try protocol.ClientHello.write(writer);
-
- try writer.writeIntLittle(u8, @as(u8, @truncate(self.config.database.len)));
- try writer.writeAll(self.config.database);
-
- try writer.writeIntLittle(u8, @as(u8, @truncate(self.config.username.len)));
- try writer.writeAll(self.config.username);
-
- try writer.writeIntLittle(u8, @as(u8, @truncate(self.config.password.len)));
- try writer.writeAll(self.config.password);
- }
-
- fn readServerHello(self: *ClickHouseClient) !void {
- var reader = self.stream.?.reader();
-
- const server_packet = try reader.readIntLittle(u64);
- if (server_packet != @intFromEnum(packet.PacketType.Hello)) {
- return ClickHouseError.ProtocolError;
- }
-
- self.server_info = try server_info.ServerInfo.read(self.allocator, reader);
- }
-
- pub fn query(self: *ClickHouseClient, query_str: []const u8) !void {
- if (self.stream == null) {
- return ClickHouseError.ConnectionFailed;
- }
-
- // Clean up previous results if any
- if (self.current_result) |*result| {
- result.deinit();
- self.current_result = null;
- }
-
- if (self.query_info) |*info| {
- _ = info;
- self.query_info = null;
- }
-
- self.query_info = query_info.QueryInfo.init();
-
- var writer = self.stream.?.writer();
-
- try packet.writePacketHeader(writer, .Query);
- try protocol.ClientInfo.write(writer, "", "ClickHouse Zig");
- try self.config.settings.write(writer);
-
- try writer.writeIntLittle(u64, query_str.len);
- try writer.writeAll(query_str);
-
- try self.processQueryResponse();
- }
-
- fn processQueryResponse(self: *ClickHouseClient) !void {
- var reader = self.stream.?.reader();
-
- while (true) {
- const packet_type = try reader.readIntLittle(u64);
-
- switch (@as(packet.PacketType, @enumFromInt(packet_type))) {
- .Data => {
- if (self.current_block == null) {
- self.current_block = block.Block.init(self.allocator);
- }
- try self.readBlock();
-
- if (self.current_block) |*b| {
- self.current_result = try results.QueryResult.init(self.allocator, b);
- }
- },
- .Progress => {
- const prog = try progress.Progress.read(reader);
- if (self.query_info) |*info| {
- info.updateProgress(prog);
- }
- },
- .ProfileInfo => {
- const prof = try profile.ProfileInfo.read(reader);
- if (self.query_info) |*info| {
- info.updateProfile(prof);
- }
- },
- .Statistics => {
- const stats = try statistics.Statistics.read(reader);
- if (self.query_info) |*info| {
- info.updateStatistics(stats);
- }
- },
- .EndOfStream => return,
- .Error => {
- const err_code = try reader.readIntLittle(u32);
- const msg_len = try reader.readIntLittle(u16);
- var msg_buf = try self.allocator.alloc(u8, msg_len);
- defer self.allocator.free(msg_buf);
- _ = try reader.readAll(msg_buf);
-
- const stack_len = try reader.readIntLittle(u16);
- var stack_buf: ?[]u8 = null;
- if (stack_len > 0) {
- stack_buf = try self.allocator.alloc(u8, stack_len);
- _ = try reader.readAll(stack_buf.?);
- }
-
- self.last_error = try cherror.Error.initWithStack(
- self.allocator,
- cherror.ErrorCode.fromInt(err_code),
- msg_buf,
- if (stack_buf) |sb| sb else "",
- );
-
- return ClickHouseError.QueryFailed;
- },
- else => {},
- }
- }
- }
-
- pub fn deinit(self: *ClickHouseClient) void {
- if (self.current_result) |*result| {
- result.deinit();
- }
- if (self.current_block) |*b| {
- b.deinit();
- }
- if (self.server_info) |*info| {
- info.deinit(self.allocator);
- }
- if (self.last_error) |err| {
- err.deinit();
- }
- if (self.stream) |stream| {
- stream.close();
- }
- }
-};
\ No newline at end of file
diff --git a/experimental/legacy_src/mutations.zig b/experimental/legacy_src/mutations.zig
deleted file mode 100644
index bf3af78..0000000
--- a/experimental/legacy_src/mutations.zig
+++ /dev/null
@@ -1,226 +0,0 @@
-const std = @import("std");
-const ClickHouseClient = @import("main.zig").ClickHouseClient;
-
-pub const MutationType = enum {
- Delete,
- Update,
- Replace,
- Clear,
-};
-
-pub const MutationStatus = struct {
- is_done: bool,
- parts_to_do: u64,
- failed_part: ?[]const u8,
- fail_reason: ?[]const u8,
- elapsed_time_ms: u64,
- latest_fail_time: ?i64,
- mutation_id: []const u8,
-};
-
-pub const Mutation = struct {
- table: []const u8,
- type: MutationType,
- condition: ?[]const u8,
- settings: MutationSettings,
- allocator: std.mem.Allocator,
- cluster: ?[]const u8,
- values: ?std.StringHashMap([]const u8),
-
- pub const MutationSettings = struct {
- replication: bool = true,
- mutations_sync: bool = false,
- timeout_ms: u64 = 10000,
- retries: u32 = 3,
- };
-
- pub fn init(
- allocator: std.mem.Allocator,
- table: []const u8,
- mutation_type: MutationType,
- ) !*Mutation {
- var mutation = try allocator.create(Mutation);
- mutation.* = .{
- .table = try allocator.dupe(u8, table),
- .type = mutation_type,
- .condition = null,
- .settings = .{},
- .allocator = allocator,
- .cluster = null,
- .values = null,
- };
- return mutation;
- }
-
- pub fn deinit(self: *Mutation) void {
- self.allocator.free(self.table);
- if (self.condition) |cond| {
- self.allocator.free(cond);
- }
- if (self.cluster) |cluster| {
- self.allocator.free(cluster);
- }
- if (self.values) |*values| {
- var it = values.iterator();
- while (it.next()) |entry| {
- self.allocator.free(entry.key_ptr.*);
- self.allocator.free(entry.value_ptr.*);
- }
- values.deinit();
- }
- self.allocator.destroy(self);
- }
-
- pub fn setCondition(self: *Mutation, condition: []const u8) !void {
- if (self.condition) |old_condition| {
- self.allocator.free(old_condition);
- }
- self.condition = try self.allocator.dupe(u8, condition);
- }
-
- pub fn setCluster(self: *Mutation, cluster: []const u8) !void {
- if (self.cluster) |old_cluster| {
- self.allocator.free(old_cluster);
- }
- self.cluster = try self.allocator.dupe(u8, cluster);
- }
-
- pub fn setValue(self: *Mutation, column: []const u8, value: []const u8) !void {
- if (self.values == null) {
- self.values = std.StringHashMap([]const u8).init(self.allocator);
- }
- const key = try self.allocator.dupe(u8, column);
- const val = try self.allocator.dupe(u8, value);
- try self.values.?.put(key, val);
- }
-
- pub fn execute(self: *Mutation, client: *ClickHouseClient) ![]const u8 {
- var query_buf = std.ArrayList(u8).init(self.allocator);
- defer query_buf.deinit();
-
- try query_buf.writer().print("ALTER TABLE {s}", .{self.table});
- if (self.cluster) |cluster| {
- try query_buf.writer().print(" ON CLUSTER {s}", .{cluster});
- }
-
- switch (self.type) {
- .Delete => {
- try query_buf.appendSlice(" DELETE");
- if (self.condition) |cond| {
- try query_buf.writer().print(" WHERE {s}", .{cond});
- }
- },
- .Update => {
- try query_buf.appendSlice(" UPDATE");
- if (self.values) |values| {
- var it = values.iterator();
- var first = true;
- while (it.next()) |entry| {
- if (!first) try query_buf.appendSlice(",");
- try query_buf.writer().print(" {s} = {s}", .{ entry.key_ptr.*, entry.value_ptr.* });
- first = false;
- }
- }
- if (self.condition) |cond| {
- try query_buf.writer().print(" WHERE {s}", .{cond});
- }
- },
- .Replace => {
- try query_buf.appendSlice(" REPLACE");
- if (self.values) |values| {
- var it = values.iterator();
- var first = true;
- while (it.next()) |entry| {
- if (!first) try query_buf.appendSlice(",");
- try query_buf.writer().print(" {s} = {s}", .{ entry.key_ptr.*, entry.value_ptr.* });
- first = false;
- }
- }
- if (self.condition) |cond| {
- try query_buf.writer().print(" WHERE {s}", .{cond});
- }
- },
- .Clear => {
- try query_buf.appendSlice(" CLEAR COLUMN");
- if (self.values) |values| {
- var it = values.iterator();
- var first = true;
- while (it.next()) |entry| {
- if (!first) try query_buf.appendSlice(",");
- try query_buf.writer().print(" {s}", .{entry.key_ptr.*});
- first = false;
- }
- }
- if (self.condition) |cond| {
- try query_buf.writer().print(" WHERE {s}", .{cond});
- }
- },
- }
-
- // Add settings
- try query_buf.appendSlice(" SETTINGS");
- try query_buf.writer().print(
- \\ mutations_sync = {d},
- \\ replication_alter_partitions_sync = {d},
- \\ mutations_timeout = {d}
- , .{
- @boolToInt(self.settings.mutations_sync),
- @boolToInt(self.settings.replication),
- self.settings.timeout_ms,
- });
-
- // Execute mutation
- try client.query(query_buf.items);
-
- // Return mutation ID for status tracking
- return try self.getMutationId(client);
- }
-
- fn getMutationId(self: *Mutation, client: *ClickHouseClient) ![]const u8 {
- var query_buf = std.ArrayList(u8).init(self.allocator);
- defer query_buf.deinit();
-
- try query_buf.writer().print(
- \\SELECT mutation_id
- \\FROM system.mutations
- \\WHERE table = '{s}'
- \\ORDER BY create_time DESC
- \\LIMIT 1
- , .{self.table});
-
- try client.query(query_buf.items);
- // Process result and return mutation ID
- return try self.allocator.dupe(u8, "mutation_id");
- }
-
- pub fn status(self: *Mutation, client: *ClickHouseClient, mutation_id: []const u8) !MutationStatus {
- var query_buf = std.ArrayList(u8).init(self.allocator);
- defer query_buf.deinit();
-
- try query_buf.writer().print(
- \\SELECT
- \\ is_done,
- \\ parts_to_do_count,
- \\ latest_failed_part,
- \\ latest_fail_reason,
- \\ elapsed,
- \\ latest_fail_time
- \\FROM system.mutations
- \\WHERE table = '{s}'
- \\ AND mutation_id = '{s}'
- , .{ self.table, mutation_id });
-
- try client.query(query_buf.items);
-
- // Process result and return status
- return MutationStatus{
- .is_done = false,
- .parts_to_do = 0,
- .failed_part = null,
- .fail_reason = null,
- .elapsed_time_ms = 0,
- .latest_fail_time = null,
- .mutation_id = try self.allocator.dupe(u8, mutation_id),
- };
- }
-};
\ No newline at end of file
diff --git a/experimental/legacy_src/packet.zig b/experimental/legacy_src/packet.zig
deleted file mode 100644
index 151d884..0000000
--- a/experimental/legacy_src/packet.zig
+++ /dev/null
@@ -1,15 +0,0 @@
-const std = @import("std");
-
-pub const PacketType = enum(u64) {
- Hello = 0,
- Data = 1,
- Query = 2,
- Error = 3,
- Progress = 4,
- Pong = 5,
- EndOfStream = 6,
-};
-
-pub fn writePacketHeader(writer: anytype, packet_type: PacketType) !void {
- try writer.writeIntLittle(u64, @intFromEnum(packet_type));
-}
\ No newline at end of file
diff --git a/experimental/legacy_src/params.zig b/experimental/legacy_src/params.zig
deleted file mode 100644
index b6a1b17..0000000
--- a/experimental/legacy_src/params.zig
+++ /dev/null
@@ -1,110 +0,0 @@
-const std = @import("std");
-const types = @import("types.zig");
-
-pub const Parameter = struct {
- name: []const u8,
- type_info: types.TypeInfo,
- value: Value,
- allocator: std.mem.Allocator,
-
- pub fn init(allocator: std.mem.Allocator, name: []const u8, value: anytype) !Parameter {
- const T = @TypeOf(value);
- const type_info = try inferTypeInfo(T);
- const param_value = try convertToValue(allocator, value);
-
- return Parameter{
- .name = try allocator.dupe(u8, name),
- .type_info = type_info,
- .value = param_value,
- .allocator = allocator,
- };
- }
-
- pub fn deinit(self: *Parameter) void {
- self.allocator.free(self.name);
- switch (self.value) {
- .String => |s| self.allocator.free(s),
- .Array => |arr| {
- for (arr) |item| {
- switch (item) {
- .String => |s| self.allocator.free(s),
- else => {},
- }
- }
- self.allocator.free(arr);
- },
- else => {},
- }
- }
-};
-
-pub const Value = union(enum) {
- Int8: i8,
- Int16: i16,
- Int32: i32,
- Int64: i64,
- UInt8: u8,
- UInt16: u16,
- UInt32: u32,
- UInt64: u64,
- Float32: f32,
- Float64: f64,
- String: []const u8,
- DateTime: i64,
- Date: i32,
- UUID: [16]u8,
- Array: []Value,
- Null,
-};
-
-fn inferTypeInfo(comptime T: type) !types.TypeInfo {
- return switch (@typeInfo(T)) {
- .Int => |info| switch (info.bits) {
- 8 => .{ .base_type = if (info.signedness == .signed) .Int8 else .UInt8 },
- 16 => .{ .base_type = if (info.signedness == .signed) .Int16 else .UInt16 },
- 32 => .{ .base_type = if (info.signedness == .signed) .Int32 else .UInt32 },
- 64 => .{ .base_type = if (info.signedness == .signed) .Int64 else .UInt64 },
- else => error.UnsupportedType,
- },
- .Float => |info| switch (info.bits) {
- 32 => .{ .base_type = .Float32 },
- 64 => .{ .base_type = .Float64 },
- else => error.UnsupportedType,
- },
- .Array => |info| if (info.child == u8) .{
- .base_type = .String,
- } else .{
- .base_type = .Array,
- .array_level = 1,
- },
- else => error.UnsupportedType,
- };
-}
-
-fn convertToValue(allocator: std.mem.Allocator, value: anytype) !Value {
- const T = @TypeOf(value);
- return switch (@typeInfo(T)) {
- .Int => |info| switch (info.bits) {
- 8 => if (info.signedness == .signed) .{ .Int8 = value } else .{ .UInt8 = value },
- 16 => if (info.signedness == .signed) .{ .Int16 = value } else .{ .UInt16 = value },
- 32 => if (info.signedness == .signed) .{ .Int32 = value } else .{ .UInt32 = value },
- 64 => if (info.signedness == .signed) .{ .Int64 = value } else .{ .UInt64 = value },
- else => error.UnsupportedType,
- },
- .Float => |info| switch (info.bits) {
- 32 => .{ .Float32 = value },
- 64 => .{ .Float64 = value },
- else => error.UnsupportedType,
- },
- .Array => |info| if (info.child == u8) .{
- .String = try allocator.dupe(u8, value),
- } else blk: {
- var array_values = try allocator.alloc(Value, value.len);
- for (value, 0..) |item, i| {
- array_values[i] = try convertToValue(allocator, item);
- }
- break :blk .{ .Array = array_values };
- },
- else => error.UnsupportedType,
- };
-}
\ No newline at end of file
diff --git a/experimental/legacy_src/pool.zig b/experimental/legacy_src/pool.zig
deleted file mode 100644
index bbf8254..0000000
--- a/experimental/legacy_src/pool.zig
+++ /dev/null
@@ -1,349 +0,0 @@
-const std = @import("std");
-const ClickHouseClient = @import("main.zig").ClickHouseClient;
-const ClickHouseConfig = @import("main.zig").ClickHouseConfig;
-
-pub const ConnectionStrategy = enum {
- Lazy, // Create connections as needed
- Eager, // Create min_connections immediately
-};
-
-pub const PoolConfig = struct {
- min_connections: usize = 2,
- max_connections: usize = 10,
- connection_timeout_ms: u64 = 5000,
- max_idle_time_ms: u64 = 300000, // 5 minutes
- health_check_interval_ms: u64 = 30000, // 30 seconds
- retry_interval_ms: u64 = 1000,
- keep_alive_interval_ms: u64 = 60000, // 1 minute
- connection_strategy: ConnectionStrategy = .Lazy,
- max_wait_queue_size: usize = 100,
-};
-
-const ConnectionState = enum {
- Idle,
- InUse,
- Broken,
- Closed,
-};
-
-pub const Pool = struct {
- allocator: std.mem.Allocator,
- config: ClickHouseConfig,
- pool_config: PoolConfig,
- connections: std.ArrayList(*Connection),
- mutex: std.Thread.Mutex,
- health_check_timer: ?std.time.Timer,
- keep_alive_timer: ?std.time.Timer,
- shutdown: std.atomic.Atomic(bool),
- wait_queue: WaitQueue,
-
- const WaitQueue = struct {
- items: std.ArrayList(WaitItem),
- mutex: std.Thread.Mutex,
-
- const WaitItem = struct {
- completion: std.Thread.Condition,
- client: ?*ClickHouseClient,
- timeout_ms: u64,
- timestamp: i64,
- };
-
- fn init(allocator: std.mem.Allocator) WaitQueue {
- return .{
- .items = std.ArrayList(WaitItem).init(allocator),
- .mutex = std.Thread.Mutex{},
- };
- }
-
- fn deinit(self: *WaitQueue) void {
- self.items.deinit();
- }
-
- fn add(self: *WaitQueue, timeout_ms: u64) !*WaitItem {
- self.mutex.lock();
- defer self.mutex.unlock();
-
- try self.items.append(.{
- .completion = std.Thread.Condition{},
- .client = null,
- .timeout_ms = timeout_ms,
- .timestamp = std.time.milliTimestamp(),
- });
-
- return &self.items.items[self.items.items.len - 1];
- }
-
- fn remove(self: *WaitQueue, item: *WaitItem) void {
- self.mutex.lock();
- defer self.mutex.unlock();
-
- for (self.items.items, 0..) |*wait_item, i| {
- if (wait_item == item) {
- _ = self.items.swapRemove(i);
- break;
- }
- }
- }
- };
-
- const Connection = struct {
- client: *ClickHouseClient,
- state: ConnectionState,
- last_used: i64,
- last_checked: i64,
-
- pub fn init(allocator: std.mem.Allocator, config: ClickHouseConfig) !*Connection {
- var conn = try allocator.create(Connection);
- var client = try allocator.create(ClickHouseClient);
- client.* = ClickHouseClient.init(allocator, config);
-
- conn.* = .{
- .client = client,
- .state = .Idle,
- .last_used = std.time.milliTimestamp(),
- .last_checked = std.time.milliTimestamp(),
- };
-
- try client.connect();
- return conn;
- }
-
- pub fn deinit(self: *Connection, allocator: std.mem.Allocator) void {
- self.client.deinit();
- allocator.destroy(self.client);
- allocator.destroy(self);
- }
-
- pub fn ping(self: *Connection) bool {
- if (self.state == .Broken or self.state == .Closed) return false;
-
- self.client.query("SELECT 1") catch {
- self.state = .Broken;
- return false;
- };
-
- self.last_checked = std.time.milliTimestamp();
- return true;
- }
-
- pub fn isHealthy(self: *Connection) bool {
- return self.state == .Idle or self.state == .InUse;
- }
-
- pub fn isExpired(self: *Connection, max_idle_time_ms: u64) bool {
- if (self.state == .InUse) return false;
- const now = std.time.milliTimestamp();
- return (now - self.last_used) > max_idle_time_ms;
- }
-
- pub fn needsHealthCheck(self: *Connection, health_check_interval_ms: u64) bool {
- const now = std.time.milliTimestamp();
- return (now - self.last_checked) > health_check_interval_ms;
- }
- };
-
- pub fn init(allocator: std.mem.Allocator, config: ClickHouseConfig, pool_config: PoolConfig) !*Pool {
- var pool = try allocator.create(Pool);
- pool.* = .{
- .allocator = allocator,
- .config = config,
- .pool_config = pool_config,
- .connections = std.ArrayList(*Connection).init(allocator),
- .mutex = std.Thread.Mutex{},
- .health_check_timer = null,
- .keep_alive_timer = null,
- .shutdown = std.atomic.Atomic(bool).init(false),
- .wait_queue = WaitQueue.init(allocator),
- };
-
- if (pool_config.connection_strategy == .Eager) {
- var i: usize = 0;
- while (i < pool_config.min_connections) : (i += 1) {
- try pool.createConnection();
- }
- }
-
- try pool.startHealthCheck();
- try pool.startKeepAlive();
-
- return pool;
- }
-
- fn createConnection(self: *Pool) !void {
- var conn = try Connection.init(self.allocator, self.config);
- try self.connections.append(conn);
- }
-
- fn startHealthCheck(self: *Pool) !void {
- self.health_check_timer = try std.time.Timer.start();
-
- const thread = try std.Thread.spawn(.{}, struct {
- fn run(p: *Pool) void {
- while (!p.shutdown.load(.Acquire)) {
- std.time.sleep(p.pool_config.health_check_interval_ms * std.time.ns_per_ms);
- p.performHealthCheck();
- }
- }
- }.run, .{self});
- thread.detach();
- }
-
- fn startKeepAlive(self: *Pool) !void {
- self.keep_alive_timer = try std.time.Timer.start();
-
- const thread = try std.Thread.spawn(.{}, struct {
- fn run(p: *Pool) void {
- while (!p.shutdown.load(.Acquire)) {
- std.time.sleep(p.pool_config.keep_alive_interval_ms * std.time.ns_per_ms);
- p.performKeepAlive();
- }
- }
- }.run, .{self});
- thread.detach();
- }
-
- fn performHealthCheck(self: *Pool) void {
- self.mutex.lock();
- defer self.mutex.unlock();
-
- var i: usize = 0;
- while (i < self.connections.items.len) {
- var conn = self.connections.items[i];
-
- if (conn.isExpired(self.pool_config.max_idle_time_ms)) {
- if (self.connections.items.len > self.pool_config.min_connections) {
- _ = self.connections.swapRemove(i);
- conn.deinit(self.allocator);
- continue;
- }
- } else if (conn.needsHealthCheck(self.pool_config.health_check_interval_ms)) {
- if (!conn.ping()) {
- self.tryReconnect(conn);
- }
- }
-
- i += 1;
- }
- }
-
- fn performKeepAlive(self: *Pool) void {
- self.mutex.lock();
- defer self.mutex.unlock();
-
- for (self.connections.items) |conn| {
- if (conn.state == .Idle) {
- _ = conn.ping();
- }
- }
- }
-
- fn tryReconnect(self: *Pool, conn: *Connection) void {
- if (conn.state == .Closed) return;
-
- conn.state = .Broken;
-
- const thread = std.Thread.spawn(.{}, struct {
- fn run(c: *Connection, config: ClickHouseConfig) void {
- c.client.deinit();
- c.client.* = ClickHouseClient.init(c.client.allocator, config);
- c.client.connect() catch {
- c.state = .Broken;
- return;
- };
- c.state = .Idle;
- c.last_checked = std.time.milliTimestamp();
- }
- }.run, .{ conn, self.config }) catch {
- conn.state = .Closed;
- return;
- };
- thread.detach();
- }
-
- pub fn acquire(self: *Pool) !*ClickHouseClient {
- while (true) {
- self.mutex.lock();
-
- // Try to find an available connection
- for (self.connections.items) |conn| {
- if (conn.state == .Idle) {
- conn.state = .InUse;
- conn.last_used = std.time.milliTimestamp();
- self.mutex.unlock();
- return conn.client;
- }
- }
-
- // Create new connection if possible
- if (self.connections.items.len < self.pool_config.max_connections) {
- var conn = Connection.init(self.allocator, self.config) catch {
- self.mutex.unlock();
- return error.ConnectionFailed;
- };
- conn.state = .InUse;
- try self.connections.append(conn);
- self.mutex.unlock();
- return conn.client;
- }
-
- self.mutex.unlock();
-
- // Wait for available connection
- if (self.wait_queue.items.items.len >= self.pool_config.max_wait_queue_size) {
- return error.WaitQueueFull;
- }
-
- var wait_item = try self.wait_queue.add(self.pool_config.connection_timeout_ms);
- defer self.wait_queue.remove(wait_item);
-
- const start = std.time.milliTimestamp();
- while (true) {
- const now = std.time.milliTimestamp();
- if (now - start >= self.pool_config.connection_timeout_ms) {
- return error.AcquireTimeout;
- }
-
- if (wait_item.client) |client| {
- return client;
- }
-
- std.time.sleep(self.pool_config.retry_interval_ms * std.time.ns_per_ms);
- }
- }
- }
-
- pub fn release(self: *Pool, client: *ClickHouseClient) void {
- self.mutex.lock();
- defer self.mutex.unlock();
-
- for (self.connections.items) |conn| {
- if (conn.client == client) {
- conn.state = .Idle;
- conn.last_used = std.time.milliTimestamp();
-
- // Check wait queue
- self.wait_queue.mutex.lock();
- defer self.wait_queue.mutex.unlock();
-
- if (self.wait_queue.items.items.len > 0) {
- var wait_item = &self.wait_queue.items.items[0];
- wait_item.client = client;
- wait_item.completion.signal();
- }
-
- break;
- }
- }
- }
-
- pub fn deinit(self: *Pool) void {
- self.shutdown.store(true, .Release);
-
- for (self.connections.items) |conn| {
- conn.deinit(self.allocator);
- }
- self.connections.deinit();
- self.wait_queue.deinit();
- self.allocator.destroy(self);
- }
-};
\ No newline at end of file
diff --git a/experimental/legacy_src/pool_config.zig b/experimental/legacy_src/pool_config.zig
deleted file mode 100644
index 9aea728..0000000
--- a/experimental/legacy_src/pool_config.zig
+++ /dev/null
@@ -1,57 +0,0 @@
-const std = @import("std");
-const RetryStrategy = @import("retry.zig").RetryStrategy;
-
-pub const PoolConfig = struct {
- min_connections: usize = 2,
- max_connections: usize = 10,
- connection_timeout_ms: u64 = 5000,
- max_idle_time_ms: u64 = 300000, // 5 minutes
- health_check_interval_ms: u64 = 30000, // 30 seconds
- retry_strategy: RetryStrategy = RetryStrategy.init(),
- max_wait_queue_size: usize = 100,
- keep_alive: bool = true,
- tcp_nodelay: bool = true,
- tcp_keepalive_time: u32 = 7200,
- tcp_keepalive_interval: u32 = 75,
- tcp_keepalive_count: u32 = 9,
-
- pub fn init() PoolConfig {
- return .{};
- }
-
- pub fn setMinConnections(self: *PoolConfig, min: usize) void {
- self.min_connections = min;
- }
-
- pub fn setMaxConnections(self: *PoolConfig, max: usize) void {
- self.max_connections = max;
- }
-
- pub fn setConnectionTimeout(self: *PoolConfig, timeout_ms: u64) void {
- self.connection_timeout_ms = timeout_ms;
- }
-
- pub fn setMaxIdleTime(self: *PoolConfig, idle_time_ms: u64) void {
- self.max_idle_time_ms = idle_time_ms;
- }
-
- pub fn setHealthCheckInterval(self: *PoolConfig, interval_ms: u64) void {
- self.health_check_interval_ms = interval_ms;
- }
-
- pub fn setRetryStrategy(self: *PoolConfig, strategy: RetryStrategy) void {
- self.retry_strategy = strategy;
- }
-
- pub fn setMaxWaitQueueSize(self: *PoolConfig, size: usize) void {
- self.max_wait_queue_size = size;
- }
-
- pub fn setTcpKeepalive(self: *PoolConfig, enable: bool) void {
- self.keep_alive = enable;
- }
-
- pub fn setTcpNodelay(self: *PoolConfig, enable: bool) void {
- self.tcp_nodelay = enable;
- }
-};
\ No newline at end of file
diff --git a/experimental/legacy_src/prepared.zig b/experimental/legacy_src/prepared.zig
deleted file mode 100644
index 2378c06..0000000
--- a/experimental/legacy_src/prepared.zig
+++ /dev/null
@@ -1,90 +0,0 @@
-const std = @import("std");
-const params = @import("params.zig");
-const types = @import("types.zig");
-
-pub const PreparedStatement = struct {
- query: []const u8,
- parameters: std.ArrayList(params.Parameter),
- allocator: std.mem.Allocator,
-
- pub fn init(allocator: std.mem.Allocator, query: []const u8) PreparedStatement {
- return .{
- .query = allocator.dupe(u8, query) catch unreachable,
- .parameters = std.ArrayList(params.Parameter).init(allocator),
- .allocator = allocator,
- };
- }
-
- pub fn deinit(self: *PreparedStatement) void {
- self.allocator.free(self.query);
- for (self.parameters.items) |*param| {
- param.deinit();
- }
- self.parameters.deinit();
- }
-
- pub fn bind(self: *PreparedStatement, name: []const u8, value: anytype) !void {
- const param = try params.Parameter.init(self.allocator, name, value);
- try self.parameters.append(param);
- }
-
- pub fn clearParameters(self: *PreparedStatement) void {
- for (self.parameters.items) |*param| {
- param.deinit();
- }
- self.parameters.clearRetainingCapacity();
- }
-
- pub fn buildQuery(self: *PreparedStatement) ![]const u8 {
- var result = std.ArrayList(u8).init(self.allocator);
- defer result.deinit();
-
- var query_iter = std.mem.split(u8, self.query, "?");
- var param_index: usize = 0;
-
- while (query_iter.next()) |part| {
- try result.appendSlice(part);
-
- if (param_index < self.parameters.items.len) {
- const param = self.parameters.items[param_index];
- try self.appendParameterValue(&result, param);
- param_index += 1;
- }
- }
-
- return result.toOwnedSlice();
- }
-
- fn appendParameterValue(self: *PreparedStatement, result: *std.ArrayList(u8), param: params.Parameter) !void {
- switch (param.value) {
- .Int8 => |v| try result.writer().print("{}", .{v}),
- .Int16 => |v| try result.writer().print("{}", .{v}),
- .Int32 => |v| try result.writer().print("{}", .{v}),
- .Int64 => |v| try result.writer().print("{}", .{v}),
- .UInt8 => |v| try result.writer().print("{}", .{v}),
- .UInt16 => |v| try result.writer().print("{}", .{v}),
- .UInt32 => |v| try result.writer().print("{}", .{v}),
- .UInt64 => |v| try result.writer().print("{}", .{v}),
- .Float32 => |v| try result.writer().print("{d}", .{v}),
- .Float64 => |v| try result.writer().print("{d}", .{v}),
- .String => |v| try result.writer().print("'{s}'", .{v}),
- .DateTime => |v| try result.writer().print("{}", .{v}),
- .Date => |v| try result.writer().print("{}", .{v}),
- .UUID => |v| try result.writer().print("{}", .{std.fmt.fmtSliceHexLower(&v)}),
- .Array => |arr| {
- try result.append('[');
- for (arr, 0..) |item, i| {
- if (i > 0) try result.appendSlice(", ");
- try self.appendParameterValue(result, .{
- .name = "",
- .type_info = param.type_info,
- .value = item,
- .allocator = self.allocator,
- });
- }
- try result.append(']');
- },
- .Null => try result.appendSlice("NULL"),
- }
- }
-};
\ No newline at end of file
diff --git a/experimental/legacy_src/profile_info.zig b/experimental/legacy_src/profile_info.zig
deleted file mode 100644
index cced7b0..0000000
--- a/experimental/legacy_src/profile_info.zig
+++ /dev/null
@@ -1,30 +0,0 @@
-const std = @import("std");
-
-pub const ProfileInfo = struct {
- rows: u64 = 0,
- blocks: u64 = 0,
- bytes: u64 = 0,
- applied_limit: bool = false,
- rows_before_limit: u64 = 0,
- calculated_rows_before_limit: bool = false,
-
- pub fn read(reader: anytype) !ProfileInfo {
- return ProfileInfo{
- .rows = try reader.readIntLittle(u64),
- .blocks = try reader.readIntLittle(u64),
- .bytes = try reader.readIntLittle(u64),
- .applied_limit = (try reader.readByte()) != 0,
- .rows_before_limit = try reader.readIntLittle(u64),
- .calculated_rows_before_limit = (try reader.readByte()) != 0,
- };
- }
-
- pub fn merge(self: *ProfileInfo, other: ProfileInfo) void {
- self.rows += other.rows;
- self.blocks += other.blocks;
- self.bytes += other.bytes;
- self.applied_limit = self.applied_limit or other.applied_limit;
- self.rows_before_limit += other.rows_before_limit;
- self.calculated_rows_before_limit = self.calculated_rows_before_limit or other.calculated_rows_before_limit;
- }
-};
\ No newline at end of file
diff --git a/experimental/legacy_src/profiling.zig b/experimental/legacy_src/profiling.zig
deleted file mode 100644
index 47547c7..0000000
--- a/experimental/legacy_src/profiling.zig
+++ /dev/null
@@ -1,112 +0,0 @@
-const std = @import("std");
-const ClickHouseClient = @import("main.zig").ClickHouseClient;
-
-pub const QueryProfile = struct {
- query_id: []const u8,
- client: *ClickHouseClient,
- allocator: std.mem.Allocator,
- settings: ProfileSettings,
-
- pub const ProfileSettings = struct {
- collect_memory_stats: bool = true,
- collect_io_stats: bool = true,
- track_memory_reference: bool = false,
- max_threads: u32 = 0,
- };
-
- pub fn init(
- client: *ClickHouseClient,
- allocator: std.mem.Allocator,
- query_id: []const u8,
- settings: ProfileSettings,
- ) !*QueryProfile {
- var profile = try allocator.create(QueryProfile);
- profile.* = .{
- .query_id = try allocator.dupe(u8, query_id),
- .client = client,
- .allocator = allocator,
- .settings = settings,
- };
- return profile;
- }
-
- pub fn deinit(self: *QueryProfile) void {
- self.allocator.free(self.query_id);
- self.allocator.destroy(self);
- }
-
- pub fn getExecutionStats(self: *QueryProfile) !ExecutionStats {
- var query_buf = std.ArrayList(u8).init(self.allocator);
- defer query_buf.deinit();
-
- try query_buf.writer().print(
- \\SELECT
- \\ query_duration_ms,
- \\ read_rows,
- \\ read_bytes,
- \\ written_rows,
- \\ written_bytes,
- \\ memory_usage,
- \\ peak_memory_usage,
- \\ thread_count
- \\FROM system.query_log
- \\WHERE query_id = '{s}'
- \\ AND type = 'QueryFinish'
- \\ORDER BY event_time DESC
- \\LIMIT 1
- , .{self.query_id});
-
- try self.client.query(query_buf.items);
-
- return ExecutionStats{
- .duration_ms = 0,
- .read_rows = 0,
- .read_bytes = 0,
- .written_rows = 0,
- .written_bytes = 0,
- .memory_usage = 0,
- .peak_memory_usage = 0,
- .thread_count = 0,
- };
- }
-
- pub fn getThreadStats(self: *QueryProfile) ![]ThreadStat {
- var query_buf = std.ArrayList(u8).init(self.allocator);
- defer query_buf.deinit();
-
- try query_buf.writer().print(
- \\SELECT
- \\ thread_id,
- \\ thread_name,
- \\ thread_duration_ms,
- \\ thread_memory_usage,
- \\ thread_peak_memory_usage
- \\FROM system.query_thread_log
- \\WHERE query_id = '{s}'
- \\ORDER BY thread_id
- , .{self.query_id});
-
- try self.client.query(query_buf.items);
-
- return &[_]ThreadStat{};
- }
-};
-
-pub const ExecutionStats = struct {
- duration_ms: u64,
- read_rows: u64,
- read_bytes: u64,
- written_rows: u64,
- written_bytes: u64,
- memory_usage: u64,
- peak_memory_usage: u64,
- thread_count: u32,
-};
-
-pub const ThreadStat = struct {
- thread_id: u32,
- thread_name: []const u8,
- duration_ms: u64,
- memory_usage: u64,
- peak_memory_usage: u64,
-};
\ No newline at end of file
diff --git a/experimental/legacy_src/progress.zig b/experimental/legacy_src/progress.zig
deleted file mode 100644
index 0f2afe0..0000000
--- a/experimental/legacy_src/progress.zig
+++ /dev/null
@@ -1,30 +0,0 @@
-const std = @import("std");
-
-pub const Progress = struct {
- rows: u64 = 0,
- bytes: u64 = 0,
- total_rows: u64 = 0,
- written_rows: u64 = 0,
- written_bytes: u64 = 0,
- elapsed_ns: u64 = 0,
-
- pub fn read(reader: anytype) !Progress {
- return Progress{
- .rows = try reader.readIntLittle(u64),
- .bytes = try reader.readIntLittle(u64),
- .total_rows = try reader.readIntLittle(u64),
- .written_rows = try reader.readIntLittle(u64),
- .written_bytes = try reader.readIntLittle(u64),
- .elapsed_ns = try reader.readIntLittle(u64),
- };
- }
-
- pub fn merge(self: *Progress, other: Progress) void {
- self.rows += other.rows;
- self.bytes += other.bytes;
- self.total_rows = other.total_rows;
- self.written_rows += other.written_rows;
- self.written_bytes += other.written_bytes;
- self.elapsed_ns = other.elapsed_ns;
- }
-};
\ No newline at end of file
diff --git a/experimental/legacy_src/protocol.zig b/experimental/legacy_src/protocol.zig
deleted file mode 100644
index 857abf0..0000000
--- a/experimental/legacy_src/protocol.zig
+++ /dev/null
@@ -1,43 +0,0 @@
-const std = @import("std");
-const compression = @import("compression.zig");
-
-pub const ClientHello = struct {
- const CLIENT_NAME = "ClickHouse Zig Client";
- const CLIENT_VERSION_MAJOR: u64 = 1;
- const CLIENT_VERSION_MINOR: u64 = 0;
- const PROTOCOL_VERSION: u64 = 54429;
-
- pub fn write(writer: anytype) !void {
- try writer.writeAll("ClickHouseClient");
- try writer.writeIntLittle(u64, CLIENT_VERSION_MAJOR);
- try writer.writeIntLittle(u64, CLIENT_VERSION_MINOR);
- try writer.writeIntLittle(u64, PROTOCOL_VERSION);
- }
-};
-
-pub const ClientInfo = struct {
- pub fn write(writer: anytype, query_id: []const u8, client_name: []const u8) !void {
- // Query ID
- try writer.writeIntLittle(u8, @as(u8, @truncate(query_id.len)));
- try writer.writeAll(query_id);
-
- // Client info block
- try writer.writeIntLittle(u8, 1); // client_info marker
- try writer.writeIntLittle(u8, 1); // major protocol version
- try writer.writeIntLittle(u8, @as(u8, @truncate(client_name.len)));
- try writer.writeAll(client_name);
- try writer.writeIntLittle(u8, 0); // query kind
-
- // Interface (TCP)
- try writer.writeIntLittle(u8, 1);
-
- // Initial address
- try writer.writeIntLittle(u8, 0);
-
- // Initial port
- try writer.writeIntLittle(u16, 0);
-
- // Compression method
- try writer.writeIntLittle(u8, @intFromEnum(compression.CompressionMethod.None));
- }
-};
\ No newline at end of file
diff --git a/experimental/legacy_src/query.zig b/experimental/legacy_src/query.zig
deleted file mode 100644
index f08eb99..0000000
--- a/experimental/legacy_src/query.zig
+++ /dev/null
@@ -1,52 +0,0 @@
-const std = @import("std");
-
-pub const QueryState = enum {
- Running,
- Cancelled,
- Completed,
- Error,
-};
-
-pub const QueryContext = struct {
- id: []const u8,
- state: QueryState,
- allocator: std.mem.Allocator,
- cancel_token: CancelToken,
-
- pub fn init(allocator: std.mem.Allocator) !*QueryContext {
- var ctx = try allocator.create(QueryContext);
- const random_bytes = try std.crypto.random.bytes(allocator, 16);
- const id = try std.fmt.allocPrint(allocator, "{}", .{std.fmt.fmtSliceHexLower(random_bytes)});
-
- ctx.* = .{
- .id = id,
- .state = .Running,
- .allocator = allocator,
- .cancel_token = CancelToken.init(),
- };
- return ctx;
- }
-
- pub fn deinit(self: *QueryContext) void {
- self.allocator.free(self.id);
- self.allocator.destroy(self);
- }
-};
-
-pub const CancelToken = struct {
- cancelled: std.atomic.Atomic(bool),
-
- pub fn init() CancelToken {
- return .{
- .cancelled = std.atomic.Atomic(bool).init(false),
- };
- }
-
- pub fn cancel(self: *CancelToken) void {
- self.cancelled.store(true, .Release);
- }
-
- pub fn isCancelled(self: *CancelToken) bool {
- return self.cancelled.load(.Acquire);
- }
-};
\ No newline at end of file
diff --git a/experimental/legacy_src/query_builder.zig b/experimental/legacy_src/query_builder.zig
deleted file mode 100644
index ab25470..0000000
--- a/experimental/legacy_src/query_builder.zig
+++ /dev/null
@@ -1,123 +0,0 @@
-const std = @import("std");
-const types = @import("types.zig");
-
-pub const QueryBuilder = struct {
- allocator: std.mem.Allocator,
- buffer: std.ArrayList(u8),
- params: std.ArrayList(Parameter),
-
- pub const Parameter = struct {
- type_info: types.TypeInfo,
- value: Value,
- };
-
- pub const Value = union(enum) {
- Int8: i8,
- Int16: i16,
- Int32: i32,
- Int64: i64,
- UInt8: u8,
- UInt16: u16,
- UInt32: u32,
- UInt64: u64,
- Float32: f32,
- Float64: f64,
- String: []const u8,
- DateTime: i64,
- Date: i32,
- UUID: [16]u8,
- Array: []const Value,
- Null,
- };
-
- pub fn init(allocator: std.mem.Allocator) QueryBuilder {
- return .{
- .allocator = allocator,
- .buffer = std.ArrayList(u8).init(allocator),
- .params = std.ArrayList(Parameter).init(allocator),
- };
- }
-
- pub fn deinit(self: *QueryBuilder) void {
- self.buffer.deinit();
- self.params.deinit();
- }
-
- pub fn reset(self: *QueryBuilder) void {
- self.buffer.clearRetainingCapacity();
- self.params.clearRetainingCapacity();
- }
-
- pub fn append(self: *QueryBuilder, sql: []const u8) !void {
- try self.buffer.appendSlice(sql);
- }
-
- pub fn param(self: *QueryBuilder, value: anytype) !void {
- const T = @TypeOf(value);
- const type_info = try self.inferTypeInfo(T);
- const param_value = try self.convertToValue(value);
-
- try self.params.append(.{
- .type_info = type_info,
- .value = param_value,
- });
-
- try self.buffer.appendSlice("?");
- }
-
- fn inferTypeInfo(self: *QueryBuilder, comptime T: type) !types.TypeInfo {
- return switch (@typeInfo(T)) {
- .Int => |info| switch (info.bits) {
- 8 => .{ .base_type = if (info.signedness == .signed) .Int8 else .UInt8 },
- 16 => .{ .base_type = if (info.signedness == .signed) .Int16 else .UInt16 },
- 32 => .{ .base_type = if (info.signedness == .signed) .Int32 else .UInt32 },
- 64 => .{ .base_type = if (info.signedness == .signed) .Int64 else .UInt64 },
- else => error.UnsupportedType,
- },
- .Float => |info| switch (info.bits) {
- 32 => .{ .base_type = .Float32 },
- 64 => .{ .base_type = .Float64 },
- else => error.UnsupportedType,
- },
- .Array => |info| if (info.child == u8) .{
- .base_type = .String,
- } else .{
- .base_type = .Array,
- .array_level = 1,
- },
- else => error.UnsupportedType,
- };
- }
-
- fn convertToValue(self: *QueryBuilder, value: anytype) !Value {
- const T = @TypeOf(value);
- return switch (@typeInfo(T)) {
- .Int => |info| switch (info.bits) {
- 8 => if (info.signedness == .signed) .{ .Int8 = value } else .{ .UInt8 = value },
- 16 => if (info.signedness == .signed) .{ .Int16 = value } else .{ .UInt16 = value },
- 32 => if (info.signedness == .signed) .{ .Int32 = value } else .{ .UInt32 = value },
- 64 => if (info.signedness == .signed) .{ .Int64 = value } else .{ .UInt64 = value },
- else => error.UnsupportedType,
- },
- .Float => |info| switch (info.bits) {
- 32 => .{ .Float32 = value },
- 64 => .{ .Float64 = value },
- else => error.UnsupportedType,
- },
- .Array => |info| if (info.child == u8) .{
- .String = value,
- } else blk: {
- var array_values = try self.allocator.alloc(Value, value.len);
- for (value, 0..) |item, i| {
- array_values[i] = try self.convertToValue(item);
- }
- break :blk .{ .Array = array_values };
- },
- else => error.UnsupportedType,
- };
- }
-
- pub fn build(self: *QueryBuilder) ![]const u8 {
- return try self.buffer.toOwnedSlice();
- }
-};
\ No newline at end of file
diff --git a/experimental/legacy_src/query_control.zig b/experimental/legacy_src/query_control.zig
deleted file mode 100644
index 3a75274..0000000
--- a/experimental/legacy_src/query_control.zig
+++ /dev/null
@@ -1,97 +0,0 @@
-const std = @import("std");
-const ClickHouseClient = @import("main.zig").ClickHouseClient;
-
-pub const QueryControl = struct {
- client: *ClickHouseClient,
- query_id: []const u8,
- allocator: std.mem.Allocator,
- timeout_ms: ?u64,
- start_time: i64,
- is_cancelled: bool,
-
- pub fn init(client: *ClickHouseClient, allocator: std.mem.Allocator, timeout_ms: ?u64) !*QueryControl {
- var ctrl = try allocator.create(QueryControl);
-
- // Generate unique query ID
- var random_bytes: [16]u8 = undefined;
- try std.crypto.random.bytes(&random_bytes);
- const id = try std.fmt.allocPrint(allocator, "q_{}", .{
- std.fmt.fmtSliceHexLower(&random_bytes),
- });
-
- ctrl.* = .{
- .client = client,
- .query_id = id,
- .allocator = allocator,
- .timeout_ms = timeout_ms,
- .start_time = std.time.milliTimestamp(),
- .is_cancelled = false,
- };
-
- return ctrl;
- }
-
- pub fn cancel(self: *QueryControl) !void {
- if (self.is_cancelled) return;
-
- var query_buf = std.ArrayList(u8).init(self.allocator);
- defer query_buf.deinit();
-
- try query_buf.writer().print(
- \\KILL QUERY WHERE query_id = '{s}'
- , .{self.query_id});
-
- try self.client.query(query_buf.items);
- self.is_cancelled = true;
- }
-
- pub fn isTimeout(self: *QueryControl) bool {
- if (self.timeout_ms) |timeout| {
- const elapsed = std.time.milliTimestamp() - self.start_time;
- return elapsed >= timeout;
- }
- return false;
- }
-
- pub fn getProgress(self: *QueryControl) !QueryProgress {
- var query_buf = std.ArrayList(u8).init(self.allocator);
- defer query_buf.deinit();
-
- try query_buf.writer().print(
- \\SELECT
- \\ read_rows,
- \\ read_bytes,
- \\ total_rows_to_read,
- \\ elapsed,
- \\ progress
- \\FROM system.processes
- \\WHERE query_id = '{s}'
- , .{self.query_id});
-
- try self.client.query(query_buf.items);
-
- return QueryProgress{
- .read_rows = 0,
- .read_bytes = 0,
- .total_rows = 0,
- .elapsed_ms = 0,
- .progress = 0,
- };
- }
-
- pub fn deinit(self: *QueryControl) void {
- if (!self.is_cancelled) {
- self.cancel() catch {};
- }
- self.allocator.free(self.query_id);
- self.allocator.destroy(self);
- }
-};
-
-pub const QueryProgress = struct {
- read_rows: u64,
- read_bytes: u64,
- total_rows: u64,
- elapsed_ms: u64,
- progress: f64,
-};
\ No newline at end of file
diff --git a/experimental/legacy_src/query_info.zig b/experimental/legacy_src/query_info.zig
deleted file mode 100644
index d1a5d37..0000000
--- a/experimental/legacy_src/query_info.zig
+++ /dev/null
@@ -1,57 +0,0 @@
-const std = @import("std");
-const progress = @import("progress.zig");
-const statistics = @import("statistics.zig");
-const profile = @import("profile_info.zig");
-
-pub const QueryInfo = struct {
- progress: progress.Progress,
- statistics: statistics.Statistics,
- profile: profile.ProfileInfo,
- written_rows: u64,
- written_bytes: u64,
-
- pub fn init() QueryInfo {
- return .{
- .progress = .{},
- .statistics = .{},
- .profile = .{},
- .written_rows = 0,
- .written_bytes = 0,
- };
- }
-
- pub fn updateProgress(self: *QueryInfo, new_progress: progress.Progress) void {
- self.progress.merge(new_progress);
- }
-
- pub fn updateStatistics(self: *QueryInfo, new_stats: statistics.Statistics) void {
- self.statistics.merge(new_stats);
- }
-
- pub fn updateProfile(self: *QueryInfo, new_profile: profile.ProfileInfo) void {
- self.profile.merge(new_profile);
- }
-
- pub fn updateWritten(self: *QueryInfo, rows: u64, bytes: u64) void {
- self.written_rows += rows;
- self.written_bytes += bytes;
- }
-
- pub fn totalRows(self: QueryInfo) u64 {
- return self.progress.rows + self.written_rows;
- }
-
- pub fn totalBytes(self: QueryInfo) u64 {
- return self.progress.bytes + self.written_bytes;
- }
-
- pub fn rowsPerSecond(self: QueryInfo) f64 {
- if (self.progress.elapsed_ns == 0) return 0;
- return @as(f64, @floatFromInt(self.totalRows())) / (@as(f64, @floatFromInt(self.progress.elapsed_ns)) / 1_000_000_000.0);
- }
-
- pub fn bytesPerSecond(self: QueryInfo) f64 {
- if (self.progress.elapsed_ns == 0) return 0;
- return @as(f64, @floatFromInt(self.totalBytes())) / (@as(f64, @floatFromInt(self.progress.elapsed_ns)) / 1_000_000_000.0);
- }
-};
\ No newline at end of file
diff --git a/experimental/legacy_src/results.zig b/experimental/legacy_src/results.zig
deleted file mode 100644
index 72d5d9d..0000000
--- a/experimental/legacy_src/results.zig
+++ /dev/null
@@ -1,202 +0,0 @@
-const std = @import("std");
-const types = @import("types.zig");
-const block = @import("block.zig");
-
-pub const QueryResult = struct {
- allocator: std.mem.Allocator,
- columns: []Column,
- rows: usize,
- current_row: usize,
-
- pub const Column = struct {
- name: []const u8,
- type: types.ClickHouseType,
- data: ColumnData,
- };
-
- pub const ColumnData = union(enum) {
- Int8: []i8,
- Int16: []i16,
- Int32: []i32,
- Int64: []i64,
- UInt8: []u8,
- UInt16: []u16,
- UInt32: []u32,
- UInt64: []u64,
- Float32: []f32,
- Float64: []f64,
- String: [][]const u8,
- DateTime: []i64,
- Date: []i32,
- UUID: [][16]u8,
- Nullable: struct {
- nulls: []bool,
- data: *const ColumnData,
- },
- };
-
- pub fn init(allocator: std.mem.Allocator, block: *block.Block) !QueryResult {
- var columns = try allocator.alloc(Column, block.columns.len);
- var total_rows: usize = 0;
-
- for (block.columns, 0..) |col, i| {
- columns[i] = try parseColumn(allocator, col);
- if (i == 0) total_rows = columnLength(columns[i].data);
- }
-
- return QueryResult{
- .allocator = allocator,
- .columns = columns,
- .rows = total_rows,
- .current_row = 0,
- };
- }
-
- fn parseColumn(allocator: std.mem.Allocator, col: block.Column) !Column {
- const parsed_data = try parseColumnData(allocator, col);
- return Column{
- .name = try allocator.dupe(u8, col.name),
- .type = col.type,
- .data = parsed_data,
- };
- }
-
- fn parseColumnData(allocator: std.mem.Allocator, col: block.Column) !ColumnData {
- return switch (col.type) {
- .Int8 => .{ .Int8 = try parseNumericColumn(i8, allocator, col.data) },
- .Int16 => .{ .Int16 = try parseNumericColumn(i16, allocator, col.data) },
- .Int32 => .{ .Int32 = try parseNumericColumn(i32, allocator, col.data) },
- .Int64 => .{ .Int64 = try parseNumericColumn(i64, allocator, col.data) },
- .UInt8 => .{ .UInt8 = try parseNumericColumn(u8, allocator, col.data) },
- .UInt16 => .{ .UInt16 = try parseNumericColumn(u16, allocator, col.data) },
- .UInt32 => .{ .UInt32 = try parseNumericColumn(u32, allocator, col.data) },
- .UInt64 => .{ .UInt64 = try parseNumericColumn(u64, allocator, col.data) },
- .Float32 => .{ .Float32 = try parseNumericColumn(f32, allocator, col.data) },
- .Float64 => .{ .Float64 = try parseNumericColumn(f64, allocator, col.data) },
- .String => .{ .String = try parseStringColumn(allocator, col.data) },
- .DateTime => .{ .DateTime = try parseNumericColumn(i64, allocator, col.data) },
- .Date => .{ .Date = try parseNumericColumn(i32, allocator, col.data) },
- .UUID => .{ .UUID = try parseUUIDColumn(allocator, col.data) },
- else => return error.UnsupportedType,
- };
- }
-
- fn parseNumericColumn(comptime T: type, allocator: std.mem.Allocator, data: []const u8) ![]T {
- const count = @divExact(data.len, @sizeOf(T));
- var result = try allocator.alloc(T, count);
- @memcpy(std.mem.sliceAsBytes(result), data);
- return result;
- }
-
- fn parseStringColumn(allocator: std.mem.Allocator, data: []const u8) ![][]const u8 {
- var strings = std.ArrayList([]const u8).init(allocator);
- defer strings.deinit();
-
- var i: usize = 0;
- while (i < data.len) {
- const len = std.mem.readIntLittle(u64, data[i..][0..8]);
- i += 8;
- const str = try allocator.dupe(u8, data[i..][0..len]);
- try strings.append(str);
- i += len;
- }
-
- return strings.toOwnedSlice();
- }
-
- fn parseUUIDColumn(allocator: std.mem.Allocator, data: []const u8) ![][16]u8 {
- const count = @divExact(data.len, 16);
- var result = try allocator.alloc([16]u8, count);
- var i: usize = 0;
- while (i < count) : (i += 1) {
- @memcpy(&result[i], data[i * 16 .. (i + 1) * 16]);
- }
- return result;
- }
-
- fn columnLength(data: ColumnData) usize {
- return switch (data) {
- .Int8 => |v| v.len,
- .Int16 => |v| v.len,
- .Int32 => |v| v.len,
- .Int64 => |v| v.len,
- .UInt8 => |v| v.len,
- .UInt16 => |v| v.len,
- .UInt32 => |v| v.len,
- .UInt64 => |v| v.len,
- .Float32 => |v| v.len,
- .Float64 => |v| v.len,
- .String => |v| v.len,
- .DateTime => |v| v.len,
- .Date => |v| v.len,
- .UUID => |v| v.len,
- .Nullable => |v| v.nulls.len,
- };
- }
-
- pub fn next(self: *QueryResult) bool {
- if (self.current_row >= self.rows) return false;
- self.current_row += 1;
- return true;
- }
-
- pub fn getValue(self: QueryResult, column_index: usize) !ValueRef {
- if (column_index >= self.columns.len) return error.InvalidColumnIndex;
- if (self.current_row == 0 or self.current_row > self.rows) return error.InvalidRowIndex;
-
- const col = self.columns[column_index];
- const row_idx = self.current_row - 1;
-
- return switch (col.data) {
- .Int8 => |v| .{ .Int8 = v[row_idx] },
- .Int16 => |v| .{ .Int16 = v[row_idx] },
- .Int32 => |v| .{ .Int32 = v[row_idx] },
- .Int64 => |v| .{ .Int64 = v[row_idx] },
- .UInt8 => |v| .{ .UInt8 = v[row_idx] },
- .UInt16 => |v| .{ .UInt16 = v[row_idx] },
- .UInt32 => |v| .{ .UInt32 = v[row_idx] },
- .UInt64 => |v| .{ .UInt64 = v[row_idx] },
- .Float32 => |v| .{ .Float32 = v[row_idx] },
- .Float64 => |v| .{ .Float64 = v[row_idx] },
- .String => |v| .{ .String = v[row_idx] },
- .DateTime => |v| .{ .DateTime = v[row_idx] },
- .Date => |v| .{ .Date = v[row_idx] },
- .UUID => |v| .{ .UUID = v[row_idx] },
- .Nullable => |v| if (v.nulls[row_idx]) .Null else try self.getValue(column_index),
- };
- }
-
- pub const ValueRef = union(enum) {
- Int8: i8,
- Int16: i16,
- Int32: i32,
- Int64: i64,
- UInt8: u8,
- UInt16: u16,
- UInt32: u32,
- UInt64: u64,
- Float32: f32,
- Float64: f64,
- String: []const u8,
- DateTime: i64,
- Date: i32,
- UUID: [16]u8,
- Null,
- };
-
- pub fn deinit(self: *QueryResult) void {
- for (self.columns) |col| {
- self.allocator.free(col.name);
- switch (col.data) {
- .String => |v| {
- for (v) |str| {
- self.allocator.free(str);
- }
- self.allocator.free(v);
- },
- inline else => |v| self.allocator.free(v),
- }
- }
- self.allocator.free(self.columns);
- }
-};
\ No newline at end of file
diff --git a/experimental/legacy_src/retry.zig b/experimental/legacy_src/retry.zig
deleted file mode 100644
index c354944..0000000
--- a/experimental/legacy_src/retry.zig
+++ /dev/null
@@ -1,62 +0,0 @@
-const std = @import("std");
-
-pub const RetryStrategy = struct {
- max_attempts: u32,
- initial_delay_ms: u64,
- max_delay_ms: u64,
- multiplier: f64,
- jitter: bool,
-
- pub fn init() RetryStrategy {
- return .{
- .max_attempts = 3,
- .initial_delay_ms = 100,
- .max_delay_ms = 10000,
- .multiplier = 2.0,
- .jitter = true,
- };
- }
-
- pub fn nextDelay(self: RetryStrategy, attempt: u32) u64 {
- if (attempt >= self.max_attempts) return 0;
-
- const base_delay = @floatToInt(u64,
- @intToFloat(f64, self.initial_delay_ms) * std.math.pow(f64, self.multiplier, @intToFloat(f64, attempt))
- );
-
- const delay = @min(base_delay, self.max_delay_ms);
-
- if (!self.jitter) return delay;
-
- // Add random jitter between 0-20%
- var random = std.crypto.random;
- const jitter = random.intRangeAtMost(u64, 0, delay / 5);
- return delay + jitter;
- }
-};
-
-pub fn retry(
- comptime T: type,
- strategy: RetryStrategy,
- context: anytype,
- operation: fn(@TypeOf(context)) T!void,
-) T!void {
- var attempt: u32 = 0;
- var last_error: T!void = undefined;
-
- while (attempt < strategy.max_attempts) : (attempt += 1) {
- operation(context) catch |err| {
- last_error = err;
-
- const delay = strategy.nextDelay(attempt);
- if (delay == 0) break;
-
- std.time.sleep(delay * std.time.ns_per_ms);
- continue;
- };
-
- return;
- }
-
- return last_error;
-}
\ No newline at end of file
diff --git a/experimental/legacy_src/sampling.zig b/experimental/legacy_src/sampling.zig
deleted file mode 100644
index 98995b8..0000000
--- a/experimental/legacy_src/sampling.zig
+++ /dev/null
@@ -1,153 +0,0 @@
-const std = @import("std");
-const ClickHouseClient = @import("main.zig").ClickHouseClient;
-
-pub const SamplingMethod = enum {
- Random,
- Deterministic,
-};
-
-pub const SamplingOptions = struct {
- method: SamplingMethod = .Deterministic,
- sample_size: f64,
- seed: ?u64 = null,
-};
-
-pub fn applySampling(
- client: *ClickHouseClient,
- query: []const u8,
- options: SamplingOptions,
- allocator: std.mem.Allocator,
-) ![]const u8 {
- var query_buf = std.ArrayList(u8).init(allocator);
- defer query_buf.deinit();
-
- // Find the FROM clause
- const from_pos = std.mem.indexOf(u8, query, "FROM") orelse
- return error.InvalidQuery;
-
- // Insert the query up to FROM
- try query_buf.appendSlice(query[0..from_pos]);
-
- // Add sampling clause
- try query_buf.appendSlice("FROM ");
-
- // Add table name and sampling
- const remaining = query[from_pos + 4 ..];
- const space_pos = std.mem.indexOf(u8, remaining, " ") orelse remaining.len;
- const table = remaining[0..space_pos];
-
- try query_buf.appendSlice(table);
-
- switch (options.method) {
- .Random => {
- try query_buf.writer().print(
- " SAMPLE {d}",
- .{options.sample_size},
- );
- if (options.seed) |seed| {
- try query_buf.writer().print(" SEED {d}", .{seed});
- }
- },
- .Deterministic => {
- try query_buf.writer().print(
- " SAMPLE {d} OFFSET {d}",
- .{ options.sample_size, if (options.seed) |s| s else 0 },
- );
- },
- }
-
- // Add the rest of the query
- if (space_pos < remaining.len) {
- try query_buf.appendSlice(remaining[space_pos..]);
- }
-
- return query_buf.toOwnedSlice();
-}
-
-pub fn estimateSampleSize(
- client: *ClickHouseClient,
- table: []const u8,
- confidence_level: f64,
- margin_of_error: f64,
- allocator: std.mem.Allocator,
-) !usize {
- var query_buf = std.ArrayList(u8).init(allocator);
- defer query_buf.deinit();
-
- // Get total count
- try query_buf.writer().print("SELECT count() FROM {s}", .{table});
- try client.query(query_buf.items);
-
- // Calculate sample size using Cochran's formula
- // For now, return a reasonable default
- return 1000;
-}
-
-pub const StratifiedSampling = struct {
- strata: []const Stratum,
- allocator: std.mem.Allocator,
-
- pub const Stratum = struct {
- column: []const u8,
- value: []const u8,
- sample_size: usize,
- };
-
- pub fn init(allocator: std.mem.Allocator, strata: []const Stratum) !StratifiedSampling {
- return StratifiedSampling{
- .strata = try allocator.dupe(Stratum, strata),
- .allocator = allocator,
- };
- }
-
- pub fn deinit(self: *StratifiedSampling) void {
- self.allocator.free(self.strata);
- }
-
- pub fn apply(
- self: *StratifiedSampling,
- client: *ClickHouseClient,
- table: []const u8,
- ) !void {
- var query_buf = std.ArrayList(u8).init(self.allocator);
- defer query_buf.deinit();
-
- // Create temporary tables for each stratum
- for (self.strata) |stratum| {
- try query_buf.writer().print(
- \\CREATE TEMPORARY TABLE tmp_{s}_{s} AS
- \\SELECT *
- \\FROM {s}
- \\WHERE {s} = '{s}'
- \\SAMPLE {d}
- , .{
- table,
- stratum.column,
- table,
- stratum.column,
- stratum.value,
- stratum.sample_size,
- });
-
- try client.query(query_buf.items);
- query_buf.clearRetainingCapacity();
- }
-
- // Union all strata
- try query_buf.writer().print(
- \\CREATE TEMPORARY TABLE {s}_stratified AS
- \\SELECT * FROM (
- , .{table});
-
- for (self.strata, 0..) |stratum, i| {
- if (i > 0) try query_buf.appendSlice("\nUNION ALL\n");
- try query_buf.writer().print(
- "SELECT * FROM tmp_{s}_{s}",
- .{ table, stratum.column },
- );
- }
-
- try query_buf.appendSlice(")");
- try client.query(query_buf.items);
- }
-};
\ No newline at end of file
diff --git a/experimental/legacy_src/serialization.zig b/experimental/legacy_src/serialization.zig
deleted file mode 100644
index c449815..0000000
--- a/experimental/legacy_src/serialization.zig
+++ /dev/null
@@ -1,218 +0,0 @@
-const std = @import("std");
-const types = @import("types.zig");
-const complex_types = @import("complex_types.zig");
-
-pub const SerializationError = error{
- InvalidType,
- BufferTooSmall,
- InvalidData,
- UnsupportedType,
-};
-
-pub fn serializeLowCardinality(writer: anytype, value: complex_types.LowCardinality) !void {
- // Write dictionary size
- try writer.writeIntLittle(u64, value.dictionary_size);
-
- // Write dictionary entries
- switch (value.base_type.base_type) {
- .String => {
- // String dictionary format:
- // - key (index)
- // - length of string
- // - string data
- for (value.dictionary) |entry| {
- try writer.writeIntLittle(u32, entry.key);
- try writer.writeIntLittle(u32, @intCast(u32, entry.value.len));
- try writer.writeAll(entry.value);
- }
- },
- .Int8, .Int16, .Int32, .Int64,
- .UInt8, .UInt16, .UInt32, .UInt64,
- .Float32, .Float64 => {
- // Numeric dictionary format:
- // - key (index)
- // - value
- for (value.dictionary) |entry| {
- try writer.writeIntLittle(u32, entry.key);
- try serializeNumeric(writer, entry.value, value.base_type.base_type);
- }
- },
- else => return SerializationError.UnsupportedType,
- }
-}
-
-pub fn deserializeLowCardinality(reader: anytype, allocator: std.mem.Allocator, type_info: types.TypeInfo) !complex_types.LowCardinality {
- const dict_size = try reader.readIntLittle(u64);
- var result = complex_types.LowCardinality.init(type_info.low_cardinality_type.?.*);
- result.dictionary_size = dict_size;
-
- var dictionary = std.ArrayList(complex_types.LowCardinality.DictionaryEntry).init(allocator);
- errdefer dictionary.deinit();
-
- var i: usize = 0;
- while (i < dict_size) : (i += 1) {
- const key = try reader.readIntLittle(u32);
- const value = switch (type_info.low_cardinality_type.?.base_type) {
- .String => {
- const len = try reader.readIntLittle(u32);
- const str = try allocator.alloc(u8, len);
- errdefer allocator.free(str);
- try reader.readNoEof(str);
- .{ .String = str };
- },
- else => try deserializeNumeric(reader, type_info.low_cardinality_type.?.base_type),
- };
-
- try dictionary.append(.{ .key = key, .value = value });
- }
-
- result.dictionary = try dictionary.toOwnedSlice();
- return result;
-}
-
-pub fn serializeNested(writer: anytype, value: complex_types.Nested) !void {
- // Write number of fields
- try writer.writeIntLittle(u32, @intCast(u32, value.types.len));
-
- // Write each field's data
- for (value.types) |field_type| {
- try writer.writeIntLittle(u32, @intCast(u32, field_type.name.len));
- try writer.writeAll(field_type.name);
- try serializeTypeInfo(writer, field_type);
- }
-}
-
-pub fn deserializeNested(reader: anytype, allocator: std.mem.Allocator) !complex_types.Nested {
- const field_count = try reader.readIntLittle(u32);
- var field_types = try allocator.alloc(types.TypeInfo, field_count);
- errdefer allocator.free(field_types);
-
- var i: usize = 0;
- while (i < field_count) : (i += 1) {
- const name_len = try reader.readIntLittle(u32);
- const name = try allocator.alloc(u8, name_len);
- errdefer allocator.free(name);
- try reader.readNoEof(name);
-
- field_types[i] = try deserializeTypeInfo(reader, allocator);
- field_types[i].name = name;
- }
-
- return complex_types.Nested.init(allocator, "nested", field_types);
-}
-
-pub fn serializeMap(writer: anytype, value: complex_types.Map) !void {
- // Write key type info
- try serializeTypeInfo(writer, value.key_type);
-
- // Write value type info
- try serializeTypeInfo(writer, value.value_type);
-
- // Write if nullable
- try writer.writeByte(if (value.nullable) 1 else 0);
-}
-
-pub fn deserializeMap(reader: anytype, allocator: std.mem.Allocator) !complex_types.Map {
- const key_type = try deserializeTypeInfo(reader, allocator);
- const value_type = try deserializeTypeInfo(reader, allocator);
- const nullable = (try reader.readByte()) != 0;
-
- var result = complex_types.Map.init(key_type, value_type);
- result.nullable = nullable;
- return result;
-}
-
-fn serializeTypeInfo(writer: anytype, type_info: types.TypeInfo) !void {
- try writer.writeIntLittle(u8, @enumToInt(type_info.base_type));
- try writer.writeByte(if (type_info.nullable) 1 else 0);
-
- if (type_info.precision) |p| {
- try writer.writeByte(1);
- try writer.writeIntLittle(u8, p);
- } else {
- try writer.writeByte(0);
- }
-
- if (type_info.scale) |s| {
- try writer.writeByte(1);
- try writer.writeIntLittle(u8, s);
- } else {
- try writer.writeByte(0);
- }
-
- try writer.writeIntLittle(u8, type_info.array_level);
-}
-
-fn deserializeTypeInfo(reader: anytype, allocator: std.mem.Allocator) !types.TypeInfo {
- const base_type = @intToEnum(types.ClickHouseType, try reader.readIntLittle(u8));
- const nullable = (try reader.readByte()) != 0;
-
- const has_precision = (try reader.readByte()) != 0;
- const precision = if (has_precision) try reader.readIntLittle(u8) else null;
-
- const has_scale = (try reader.readByte()) != 0;
- const scale = if (has_scale) try reader.readIntLittle(u8) else null;
-
- const array_level = try reader.readIntLittle(u8);
-
- return types.TypeInfo{
- .base_type = base_type,
- .nullable = nullable,
- .precision = precision,
- .scale = scale,
- .array_level = array_level,
- .fixed_string_length = null,
- .enum_values = null,
- .tuple_types = null,
- .map_key_type = null,
- .map_value_type = null,
- .low_cardinality_type = null,
- .nested_types = null,
- };
-}
-
-fn serializeNumeric(writer: anytype, value: anytype, base_type: types.ClickHouseType) !void {
- switch (base_type) {
- .Int8 => try writer.writeIntLittle(i8, value),
- .Int16 => try writer.writeIntLittle(i16, value),
- .Int32 => try writer.writeIntLittle(i32, value),
- .Int64 => try writer.writeIntLittle(i64, value),
- .UInt8 => try writer.writeIntLittle(u8, value),
- .UInt16 => try writer.writeIntLittle(u16, value),
- .UInt32 => try writer.writeIntLittle(u32, value),
- .UInt64 => try writer.writeIntLittle(u64, value),
- .Float32 => try writer.writeIntLittle(f32, value),
- .Float64 => try writer.writeIntLittle(f64, value),
- else => return SerializationError.UnsupportedType,
- }
-}
-
-fn deserializeNumeric(reader: anytype, base_type: types.ClickHouseType) !Value {
- return switch (base_type) {
- .Int8 => .{ .Int8 = try reader.readIntLittle(i8) },
- .Int16 => .{ .Int16 = try reader.readIntLittle(i16) },
- .Int32 => .{ .Int32 = try reader.readIntLittle(i32) },
- .Int64 => .{ .Int64 = try reader.readIntLittle(i64) },
- .UInt8 => .{ .UInt8 = try reader.readIntLittle(u8) },
- .UInt16 => .{ .UInt16 = try reader.readIntLittle(u16) },
- .UInt32 => .{ .UInt32 = try reader.readIntLittle(u32) },
- .UInt64 => .{ .UInt64 = try reader.readIntLittle(u64) },
- .Float32 => .{ .Float32 = try reader.readIntLittle(f32) },
- .Float64 => .{ .Float64 = try reader.readIntLittle(f64) },
- else => SerializationError.UnsupportedType,
- };
-}
-
-pub const Value = union(enum) {
- Int8: i8,
- Int16: i16,
- Int32: i32,
- Int64: i64,
- UInt8: u8,
- UInt16: u16,
- UInt32: u32,
- UInt64: u64,
- Float32: f32,
- Float64: f64,
- String: []const u8,
-};
\ No newline at end of file
diff --git a/experimental/legacy_src/server_info.zig b/experimental/legacy_src/server_info.zig
deleted file mode 100644
index 757578e..0000000
--- a/experimental/legacy_src/server_info.zig
+++ /dev/null
@@ -1,47 +0,0 @@
-const std = @import("std");
-
-pub const ServerInfo = struct {
- name: []const u8,
- major_version: u64,
- minor_version: u64,
- revision: u64,
- timezone: []const u8,
- display_name: []const u8,
- version_patch: u64,
-
- pub fn read(allocator: std.mem.Allocator, reader: anytype) !ServerInfo {
- var name_len = try reader.readIntLittle(u8);
- var name_buf = try allocator.alloc(u8, name_len);
- _ = try reader.readAll(name_buf);
-
- const major_version = try reader.readIntLittle(u64);
- const minor_version = try reader.readIntLittle(u64);
- const revision = try reader.readIntLittle(u64);
-
- var tz_len = try reader.readIntLittle(u8);
- var tz_buf = try allocator.alloc(u8, tz_len);
- _ = try reader.readAll(tz_buf);
-
- var display_len = try reader.readIntLittle(u8);
- var display_buf = try allocator.alloc(u8, display_len);
- _ = try reader.readAll(display_buf);
-
- const version_patch = try reader.readIntLittle(u64);
-
- return ServerInfo{
- .name = name_buf,
- .major_version = major_version,
- .minor_version = minor_version,
- .revision = revision,
- .timezone = tz_buf,
- .display_name = display_buf,
- .version_patch = version_patch,
- };
- }
-
- pub fn deinit(self: *ServerInfo, allocator: std.mem.Allocator) void {
- allocator.free(self.name);
- allocator.free(self.timezone);
- allocator.free(self.display_name);
- }
-};
\ No newline at end of file
diff --git a/experimental/legacy_src/settings.zig b/experimental/legacy_src/settings.zig
deleted file mode 100644
index b848c9d..0000000
--- a/experimental/legacy_src/settings.zig
+++ /dev/null
@@ -1,112 +0,0 @@
-const std = @import("std");
-
-pub const Settings = struct {
- // Connection settings
- max_block_size: u64 = 65536,
- connect_timeout_ms: u64 = 10000,
- receive_timeout_ms: u64 = 10000,
- send_timeout_ms: u64 = 10000,
- tcp_keep_alive: bool = true,
- tcp_nodelay: bool = true,
- compression_method: u8 = 0,
- decompress_response: bool = true,
-
- // Query settings
- max_insert_block_size: u64 = 1048576,
- max_threads: u32 = 8,
- max_memory_usage: u64 = 0,
- prefer_localhost_replica: bool = true,
- totals_mode: TotalsMode = .AfterHavingGroupBy,
- quota_key: ?[]const u8 = null,
- priority: u32 = 0,
- load_balancing: LoadBalancing = .Random,
- max_execution_time: u64 = 0,
- max_rows_to_read: u64 = 0,
- max_bytes_to_read: u64 = 0,
- max_result_rows: u64 = 0,
- max_result_bytes: u64 = 0,
- result_overflow_mode: OverflowMode = .Break,
-
- pub const TotalsMode = enum {
- BeforeHavingGroupBy,
- AfterHavingGroupBy,
- OnlyFinal,
- };
-
- pub const LoadBalancing = enum {
- Random,
- NearestHost,
- InOrder,
- FirstOrRandom,
- };
-
- pub const OverflowMode = enum {
- Break,
- Throw,
- Any,
- };
-
- pub fn write(self: Settings, writer: anytype) !void {
- var settings_count: usize = 0;
- var settings_buf = std.ArrayList(u8).init(std.heap.page_allocator);
- defer settings_buf.deinit();
-
- // Write each setting
- inline for (std.meta.fields(Settings)) |field| {
- const value = @field(self, field.name);
- if (shouldWriteSetting(field.type, value)) {
- try writeSettingValue(&settings_buf, field.name, value);
- settings_count += 1;
- }
- }
-
- // Write settings count
- try writer.writeIntLittle(u64, settings_count);
-
- // Write settings buffer
- try writer.writeAll(settings_buf.items);
- }
-
- fn shouldWriteSetting(comptime T: type, value: T) bool {
- return switch (@typeInfo(T)) {
- .Optional => value != null,
- .Int, .Float => value != 0,
- .Bool => value != false,
- .Enum => true,
- else => false,
- };
- }
-
- fn writeSettingValue(buf: *std.ArrayList(u8), name: []const u8, value: anytype) !void {
- try buf.writer().writeIntLittle(u8, @intCast(u8, name.len));
- try buf.writer().writeAll(name);
-
- const T = @TypeOf(value);
- switch (@typeInfo(T)) {
- .Int => |info| {
- try buf.writer().writeIntLittle(u8, if (info.bits <= 8) 0 else 1);
- if (info.bits <= 8) {
- try buf.writer().writeIntLittle(u8, @intCast(u8, value));
- } else {
- try buf.writer().writeIntLittle(u64, value);
- }
- },
- .Float => {
- try buf.writer().writeIntLittle(u8, 3);
- try buf.writer().writeIntLittle(f64, value);
- },
- .Bool => {
- try buf.writer().writeIntLittle(u8, 0);
- try buf.writer().writeIntLittle(u8, @boolToInt(value));
- },
- .Enum => {
- try buf.writer().writeIntLittle(u8, 2);
- try buf.writer().writeAll(@tagName(value));
- },
- .Optional => if (value) |v| {
- try writeSettingValue(buf, name, v);
- },
- else => unreachable,
- }
- }
-};
\ No newline at end of file
diff --git a/experimental/legacy_src/statistics.zig b/experimental/legacy_src/statistics.zig
deleted file mode 100644
index 699d5de..0000000
--- a/experimental/legacy_src/statistics.zig
+++ /dev/null
@@ -1,34 +0,0 @@
-const std = @import("std");
-
-pub const Statistics = struct {
- rows_read: u64 = 0,
- bytes_read: u64 = 0,
- total_rows_approx: u64 = 0,
- elapsed_ns: u64 = 0,
-
- pub fn read(reader: anytype) !Statistics {
- return Statistics{
- .rows_read = try reader.readIntLittle(u64),
- .bytes_read = try reader.readIntLittle(u64),
- .total_rows_approx = try reader.readIntLittle(u64),
- .elapsed_ns = try reader.readIntLittle(u64),
- };
- }
-
- pub fn merge(self: *Statistics, other: Statistics) void {
- self.rows_read += other.rows_read;
- self.bytes_read += other.bytes_read;
- self.total_rows_approx = other.total_rows_approx;
- self.elapsed_ns = other.elapsed_ns;
- }
-
- pub fn rowsPerSecond(self: Statistics) f64 {
- if (self.elapsed_ns == 0) return 0;
- return @as(f64, @floatFromInt(self.rows_read)) / (@as(f64, @floatFromInt(self.elapsed_ns)) / 1_000_000_000.0);
- }
-
- pub fn bytesPerSecond(self: Statistics) f64 {
- if (self.elapsed_ns == 0) return 0;
- return @as(f64, @floatFromInt(self.bytes_read)) / (@as(f64, @floatFromInt(self.elapsed_ns)) / 1_000_000_000.0);
- }
-};
\ No newline at end of file
diff --git a/experimental/legacy_src/stream.zig b/experimental/legacy_src/stream.zig
deleted file mode 100644
index 3739ecc..0000000
--- a/experimental/legacy_src/stream.zig
+++ /dev/null
@@ -1,59 +0,0 @@
-const std = @import("std");
-const block = @import("block.zig");
-const results = @import("results.zig");
-
-pub const RowStream = struct {
- allocator: std.mem.Allocator,
- current_block: ?*block.Block,
- current_row: usize,
- total_rows: usize,
- buffer_size: usize,
-
- pub fn init(allocator: std.mem.Allocator, buffer_size: usize) RowStream {
- return .{
- .allocator = allocator,
- .current_block = null,
- .current_row = 0,
- .total_rows = 0,
- .buffer_size = buffer_size,
- };
- }
-
- pub fn deinit(self: *RowStream) void {
- if (self.current_block) |b| {
- b.deinit();
- self.allocator.destroy(b);
- self.current_block = null;
- }
- }
-
- pub fn next(self: *RowStream) !?results.Row {
- if (self.current_block == null or self.current_row >= self.current_block.?.rows) {
- if (!try self.fetchNextBlock()) {
- return null;
- }
- }
-
- const row = try results.Row.fromBlock(self.allocator, self.current_block.?, self.current_row);
- self.current_row += 1;
- return row;
- }
-
- pub fn fetchNextBlock(self: *RowStream) !bool {
- // This would be implemented by the client to fetch the next block of data
- // For now, return false to indicate no more blocks
- return false;
- }
-
- pub const Iterator = struct {
- stream: *RowStream,
-
- pub fn next(self: *Iterator) !?results.Row {
- return self.stream.next();
- }
- };
-
- pub fn iterator(self: *RowStream) Iterator {
- return .{ .stream = self };
- }
-};
\ No newline at end of file
diff --git a/experimental/legacy_src/stream_buffer.zig b/experimental/legacy_src/stream_buffer.zig
deleted file mode 100644
index 0c30bbb..0000000
--- a/experimental/legacy_src/stream_buffer.zig
+++ /dev/null
@@ -1,117 +0,0 @@
-const std = @import("std");
-const block = @import("block.zig");
-const results = @import("results.zig");
-
-pub const StreamBuffer = struct {
- const default_capacity = 1000;
- const BlockNode = struct {
- block: *block.Block,
- next: ?*BlockNode,
- };
-
- allocator: std.mem.Allocator,
- head: ?*BlockNode,
- tail: ?*BlockNode,
- capacity: usize,
- size: usize,
- mutex: std.Thread.Mutex,
- not_empty: std.Thread.Condition,
- not_full: std.Thread.Condition,
-
- pub fn init(allocator: std.mem.Allocator) StreamBuffer {
- return .{
- .allocator = allocator,
- .head = null,
- .tail = null,
- .capacity = default_capacity,
- .size = 0,
- .mutex = std.Thread.Mutex{},
- .not_empty = std.Thread.Condition{},
- .not_full = std.Thread.Condition{},
- };
- }
-
- pub fn deinit(self: *StreamBuffer) void {
- var current = self.head;
- while (current) |node| {
- const next = node.next;
- node.block.deinit();
- self.allocator.destroy(node.block);
- self.allocator.destroy(node);
- current = next;
- }
- }
-
- pub fn push(self: *StreamBuffer, b: *block.Block) !void {
- self.mutex.lock();
- defer self.mutex.unlock();
-
- while (self.size >= self.capacity) {
- self.not_full.wait(&self.mutex);
- }
-
- var node = try self.allocator.create(BlockNode);
- node.block = b;
- node.next = null;
-
- if (self.tail) |tail| {
- tail.next = node;
- self.tail = node;
- } else {
- self.head = node;
- self.tail = node;
- }
-
- self.size += 1;
- self.not_empty.signal();
- }
-
- pub fn pop(self: *StreamBuffer) ?*block.Block {
- self.mutex.lock();
- defer self.mutex.unlock();
-
- while (self.size == 0) {
- self.not_empty.wait(&self.mutex);
- }
-
- if (self.head) |node| {
- const block = node.block;
- self.head = node.next;
- if (self.head == null) {
- self.tail = null;
- }
- self.allocator.destroy(node);
- self.size -= 1;
- self.not_full.signal();
- return block;
- }
-
- return null;
- }
-
- pub fn setCapacity(self: *StreamBuffer, capacity: usize) void {
- self.mutex.lock();
- defer self.mutex.unlock();
- self.capacity = capacity;
- self.not_full.broadcast();
- }
-
- pub fn clear(self: *StreamBuffer) void {
- self.mutex.lock();
- defer self.mutex.unlock();
-
- var current = self.head;
- while (current) |node| {
- const next = node.next;
- node.block.deinit();
- self.allocator.destroy(node.block);
- self.allocator.destroy(node);
- current = next;
- }
-
- self.head = null;
- self.tail = null;
- self.size = 0;
- self.not_full.broadcast();
- }
-};
\ No newline at end of file
diff --git a/experimental/legacy_src/tls.zig b/experimental/legacy_src/tls.zig
deleted file mode 100644
index 5490517..0000000
--- a/experimental/legacy_src/tls.zig
+++ /dev/null
@@ -1,105 +0,0 @@
-const std = @import("std");
-const ssl = @cImport({
- @cInclude("openssl/ssl.h");
- @cInclude("openssl/err.h");
-});
-
-pub const TlsConfig = struct {
- cert_path: ?[]const u8 = null,
- key_path: ?[]const u8 = null,
- ca_path: ?[]const u8 = null,
- verify_peer: bool = true,
- verify_host: bool = true,
- protocols: []const []const u8 = &[_][]const u8{"TLSv1.2", "TLSv1.3"},
-
- pub fn init() TlsConfig {
- return .{};
- }
-};
-
-pub const TlsContext = struct {
- ssl_ctx: *ssl.SSL_CTX,
- ssl: *ssl.SSL,
- allocator: std.mem.Allocator,
-
- pub fn init(allocator: std.mem.Allocator, config: TlsConfig) !*TlsContext {
- var ctx = try allocator.create(TlsContext);
- errdefer allocator.destroy(ctx);
-
- ssl.SSL_library_init();
- ssl.SSL_load_error_strings();
-
- const method = ssl.TLS_client_method();
- ctx.ssl_ctx = ssl.SSL_CTX_new(method) orelse return error.SslContextCreateFailed;
- errdefer ssl.SSL_CTX_free(ctx.ssl_ctx);
-
- if (config.verify_peer) {
- ssl.SSL_CTX_set_verify(ctx.ssl_ctx, ssl.SSL_VERIFY_PEER, null);
- }
-
- if (config.ca_path) |ca| {
- if (ssl.SSL_CTX_load_verify_locations(ctx.ssl_ctx, ca.ptr, null) != 1) {
- return error.LoadCaFailed;
- }
- }
-
- if (config.cert_path) |cert| {
- if (ssl.SSL_CTX_use_certificate_file(ctx.ssl_ctx, cert.ptr, ssl.SSL_FILETYPE_PEM) != 1) {
- return error.LoadCertFailed;
- }
- }
-
- if (config.key_path) |key| {
- if (ssl.SSL_CTX_use_PrivateKey_file(ctx.ssl_ctx, key.ptr, ssl.SSL_FILETYPE_PEM) != 1) {
- return error.LoadKeyFailed;
- }
- }
-
- ctx.ssl = ssl.SSL_new(ctx.ssl_ctx) orelse return error.SslCreateFailed;
- ctx.allocator = allocator;
-
- return ctx;
- }
-
- pub fn deinit(self: *TlsContext) void {
- ssl.SSL_free(self.ssl);
- ssl.SSL_CTX_free(self.ssl_ctx);
- self.allocator.destroy(self);
- }
-
- pub fn connect(self: *TlsContext, stream: std.net.Stream) !void {
- ssl.SSL_set_fd(self.ssl, @intCast(c_int, stream.handle));
-
- if (ssl.SSL_connect(self.ssl) != 1) {
- return error.TlsConnectFailed;
- }
- }
-
- pub fn read(self: *TlsContext, buffer: []u8) !usize {
- const result = ssl.SSL_read(self.ssl, buffer.ptr, @intCast(c_int, buffer.len));
- if (result <= 0) {
- const err = ssl.SSL_get_error(self.ssl, result);
- return switch (err) {
- ssl.SSL_ERROR_WANT_READ => error.WouldBlock,
- ssl.SSL_ERROR_WANT_WRITE => error.WouldBlock,
- ssl.SSL_ERROR_ZERO_RETURN => error.ConnectionClosed,
- else => error.TlsReadFailed,
- };
- }
- return @intCast(usize, result);
- }
-
- pub fn write(self: *TlsContext, buffer: []const u8) !usize {
- const result = ssl.SSL_write(self.ssl, buffer.ptr, @intCast(c_int, buffer.len));
- if (result <= 0) {
- const err = ssl.SSL_get_error(self.ssl, result);
- return switch (err) {
- ssl.SSL_ERROR_WANT_READ => error.WouldBlock,
- ssl.SSL_ERROR_WANT_WRITE => error.WouldBlock,
- ssl.SSL_ERROR_ZERO_RETURN => error.ConnectionClosed,
- else => error.TlsWriteFailed,
- };
- }
- return @intCast(usize, result);
- }
-};
\ No newline at end of file
diff --git a/experimental/legacy_src/transaction.zig b/experimental/legacy_src/transaction.zig
deleted file mode 100644
index 8a7c6ee..0000000
--- a/experimental/legacy_src/transaction.zig
+++ /dev/null
@@ -1,74 +0,0 @@
-const std = @import("std");
-const ClickHouseClient = @import("main.zig").ClickHouseClient;
-
-pub const Transaction = struct {
- client: *ClickHouseClient,
- id: []const u8,
- allocator: std.mem.Allocator,
- is_active: bool,
-
- pub fn begin(client: *ClickHouseClient, allocator: std.mem.Allocator) !*Transaction {
- var tx = try allocator.create(Transaction);
-
- // Generate unique transaction ID
- var random_bytes: [16]u8 = undefined;
- try std.crypto.random.bytes(&random_bytes);
- const tx_id = try std.fmt.allocPrint(allocator, "tx_{}", .{
- std.fmt.fmtSliceHexLower(&random_bytes),
- });
-
- tx.* = .{
- .client = client,
- .id = tx_id,
- .allocator = allocator,
- .is_active = true,
- };
-
- // Start transaction
- try client.query("BEGIN TRANSACTION");
-
- return tx;
- }
-
- pub fn commit(self: *Transaction) !void {
- if (!self.is_active) return error.TransactionNotActive;
-
- try self.client.query("COMMIT");
- self.is_active = false;
- }
-
- pub fn rollback(self: *Transaction) !void {
- if (!self.is_active) return error.TransactionNotActive;
-
- try self.client.query("ROLLBACK");
- self.is_active = false;
- }
-
- pub fn query(self: *Transaction, sql: []const u8) !void {
- if (!self.is_active) return error.TransactionNotActive;
-
- // Execute query within transaction
- try self.client.query(sql);
- }
-
- pub fn deinit(self: *Transaction) void {
- if (self.is_active) {
- self.rollback() catch {};
- }
- self.allocator.free(self.id);
- self.allocator.destroy(self);
- }
-};
-
-pub const TransactionOptions = struct {
- isolation_level: IsolationLevel = .ReadCommitted,
- read_only: bool = false,
- timeout_ms: u64 = 30000,
-
- pub const IsolationLevel = enum {
- ReadUncommitted,
- ReadCommitted,
- RepeatableRead,
- Serializable,
- };
-};
\ No newline at end of file
diff --git a/experimental/legacy_src/types.zig b/experimental/legacy_src/types.zig
deleted file mode 100644
index f3fe386..0000000
--- a/experimental/legacy_src/types.zig
+++ /dev/null
@@ -1,208 +0,0 @@
-const std = @import("std");
-
-pub const ClickHouseType = enum {
- Int8,
- Int16,
- Int32,
- Int64,
- UInt8,
- UInt16,
- UInt32,
- UInt64,
- Float32,
- Float64,
- String,
- FixedString,
- DateTime,
- Date,
- UUID,
- Array,
- Nullable,
- Decimal,
- Decimal32,
- Decimal64,
- Decimal128,
- Enum8,
- Enum16,
- Map,
- Tuple,
- LowCardinality,
- Nested,
-
- pub fn fromStr(type_str: []const u8) !ClickHouseType {
- if (std.mem.eql(u8, type_str, "Int8")) return .Int8;
- if (std.mem.eql(u8, type_str, "Int16")) return .Int16;
- if (std.mem.eql(u8, type_str, "Int32")) return .Int32;
- if (std.mem.eql(u8, type_str, "Int64")) return .Int64;
- if (std.mem.eql(u8, type_str, "UInt8")) return .UInt8;
- if (std.mem.eql(u8, type_str, "UInt16")) return .UInt16;
- if (std.mem.eql(u8, type_str, "UInt32")) return .UInt32;
- if (std.mem.eql(u8, type_str, "UInt64")) return .UInt64;
- if (std.mem.eql(u8, type_str, "Float32")) return .Float32;
- if (std.mem.eql(u8, type_str, "Float64")) return .Float64;
- if (std.mem.eql(u8, type_str, "String")) return .String;
- if (std.mem.startsWith(u8, type_str, "FixedString")) return .FixedString;
- if (std.mem.eql(u8, type_str, "DateTime")) return .DateTime;
- if (std.mem.eql(u8, type_str, "Date")) return .Date;
- if (std.mem.eql(u8, type_str, "UUID")) return .UUID;
- if (std.mem.startsWith(u8, type_str, "Array")) return .Array;
- if (std.mem.startsWith(u8, type_str, "Nullable")) return .Nullable;
- if (std.mem.startsWith(u8, type_str, "Decimal")) {
- if (std.mem.startsWith(u8, type_str, "Decimal32")) return .Decimal32;
- if (std.mem.startsWith(u8, type_str, "Decimal64")) return .Decimal64;
- if (std.mem.startsWith(u8, type_str, "Decimal128")) return .Decimal128;
- return .Decimal;
- }
- if (std.mem.startsWith(u8, type_str, "Enum8")) return .Enum8;
- if (std.mem.startsWith(u8, type_str, "Enum16")) return .Enum16;
- if (std.mem.startsWith(u8, type_str, "Map")) return .Map;
- if (std.mem.startsWith(u8, type_str, "Tuple")) return .Tuple;
- if (std.mem.startsWith(u8, type_str, "LowCardinality")) return .LowCardinality;
- if (std.mem.startsWith(u8, type_str, "Nested")) return .Nested;
- return error.UnsupportedType;
- }
-
- pub fn isDecimal(self: ClickHouseType) bool {
- return switch (self) {
- .Decimal, .Decimal32, .Decimal64, .Decimal128 => true,
- else => false,
- };
- }
-
- pub fn isEnum(self: ClickHouseType) bool {
- return switch (self) {
- .Enum8, .Enum16 => true,
- else => false,
- };
- }
-
- pub fn isComplex(self: ClickHouseType) bool {
- return switch (self) {
- .Array, .Nullable, .Map, .Tuple, .LowCardinality, .Nested => true,
- else => false,
- };
- }
-};
-
-pub const TypeInfo = struct {
- base_type: ClickHouseType,
- nullable: bool = false,
- precision: ?u8 = null,
- scale: ?u8 = null,
- array_level: u8 = 0,
- fixed_string_length: ?u32 = null,
- enum_values: ?[]const EnumValue = null,
- tuple_types: ?[]const TypeInfo = null,
- map_key_type: ?*const TypeInfo = null,
- map_value_type: ?*const TypeInfo = null,
- low_cardinality_type: ?*const TypeInfo = null,
- nested_types: ?[]const NestedField = null,
-
- pub const EnumValue = struct {
- name: []const u8,
- value: i16,
- };
-
- pub const NestedField = struct {
- name: []const u8,
- type_info: TypeInfo,
- };
-
- pub fn parse(allocator: std.mem.Allocator, type_str: []const u8) !TypeInfo {
- var result = TypeInfo{
- .base_type = undefined,
- };
-
- if (std.mem.startsWith(u8, type_str, "Nullable(")) {
- result.nullable = true;
- const inner_type = type_str[9 .. type_str.len - 1];
- const inner_info = try parse(allocator, inner_type);
- result.base_type = inner_info.base_type;
- result.precision = inner_info.precision;
- result.scale = inner_info.scale;
- result.array_level = inner_info.array_level;
- result.fixed_string_length = inner_info.fixed_string_length;
- result.enum_values = inner_info.enum_values;
- result.tuple_types = inner_info.tuple_types;
- return result;
- }
-
- if (std.mem.startsWith(u8, type_str, "Array(")) {
- var inner_type = type_str[6 .. type_str.len - 1];
- var inner_info = try parse(allocator, inner_type);
- inner_info.array_level += 1;
- return inner_info;
- }
-
- if (std.mem.startsWith(u8, type_str, "LowCardinality(")) {
- const inner_type = type_str[14 .. type_str.len - 1];
- var inner_info = try parse(allocator, inner_type);
- result.base_type = .LowCardinality;
- result.low_cardinality_type = try allocator.create(TypeInfo);
- result.low_cardinality_type.?.* = inner_info;
- return result;
- }
-
- if (std.mem.startsWith(u8, type_str, "Map(")) {
- result.base_type = .Map;
- const map_types = type_str[4 .. type_str.len - 1];
- var it = std.mem.split(u8, map_types, ",");
- const key_type_str = it.next() orelse return error.InvalidMapType;
- const value_type_str = it.next() orelse return error.InvalidMapType;
-
- result.map_key_type = try allocator.create(TypeInfo);
- result.map_value_type = try allocator.create(TypeInfo);
- result.map_key_type.?.* = try parse(allocator, std.mem.trim(u8, key_type_str, " "));
- result.map_value_type.?.* = try parse(allocator, std.mem.trim(u8, value_type_str, " "));
- return result;
- }
-
- if (std.mem.startsWith(u8, type_str, "Nested(")) {
- result.base_type = .Nested;
- const nested_fields = type_str[7 .. type_str.len - 1];
- var fields = std.ArrayList(NestedField).init(allocator);
- defer fields.deinit();
-
- var it = std.mem.split(u8, nested_fields, ",");
- while (it.next()) |field| {
- const trimmed = std.mem.trim(u8, field, " ");
- var field_parts = std.mem.split(u8, trimmed, " ");
- const field_name = field_parts.next() orelse return error.InvalidNestedType;
- const field_type = field_parts.next() orelse return error.InvalidNestedType;
-
- try fields.append(.{
- .name = try allocator.dupe(u8, field_name),
- .type_info = try parse(allocator, field_type),
- });
- }
-
- result.nested_types = try fields.toOwnedSlice();
- return result;
- }
-
- result.base_type = try ClickHouseType.fromStr(type_str);
- return result;
- }
-
- pub fn deinit(self: *TypeInfo, allocator: std.mem.Allocator) void {
- if (self.map_key_type) |key_type| {
- key_type.deinit(allocator);
- allocator.destroy(key_type);
- }
- if (self.map_value_type) |value_type| {
- value_type.deinit(allocator);
- allocator.destroy(value_type);
- }
- if (self.low_cardinality_type) |lc_type| {
- lc_type.deinit(allocator);
- allocator.destroy(lc_type);
- }
- if (self.nested_types) |nested| {
- for (nested) |*field| {
- allocator.free(field.name);
- field.type_info.deinit(allocator);
- }
- allocator.free(nested);
- }
- }
-};
\ No newline at end of file
diff --git a/experimental/legacy_src/views.zig b/experimental/legacy_src/views.zig
deleted file mode 100644
index 35baa7b..0000000
--- a/experimental/legacy_src/views.zig
+++ /dev/null
@@ -1,155 +0,0 @@
-const std = @import("std");
-const ClickHouseClient = @import("main.zig").ClickHouseClient;
-
-pub const MaterializedView = struct {
- name: []const u8,
- target_table: []const u8,
- query: []const u8,
- populate: bool,
- cluster: ?[]const u8,
- allocator: std.mem.Allocator,
-
- pub fn init(
- allocator: std.mem.Allocator,
- name: []const u8,
- target_table: []const u8,
- query: []const u8,
- ) !*MaterializedView {
- var view = try allocator.create(MaterializedView);
- view.* = .{
- .name = try allocator.dupe(u8, name),
- .target_table = try allocator.dupe(u8, target_table),
- .query = try allocator.dupe(u8, query),
- .populate = false,
- .cluster = null,
- .allocator = allocator,
- };
- return view;
- }
-
- pub fn deinit(self: *MaterializedView) void {
- self.allocator.free(self.name);
- self.allocator.free(self.target_table);
- self.allocator.free(self.query);
- if (self.cluster) |cluster| {
- self.allocator.free(cluster);
- }
- self.allocator.destroy(self);
- }
-
- pub fn setCluster(self: *MaterializedView, cluster: []const u8) !void {
- if (self.cluster) |old_cluster| {
- self.allocator.free(old_cluster);
- }
- self.cluster = try self.allocator.dupe(u8, cluster);
- }
-
- pub fn create(self: *MaterializedView, client: *ClickHouseClient) !void {
- var query_buf = std.ArrayList(u8).init(self.allocator);
- defer query_buf.deinit();
-
- try query_buf.writer().print(
- \\CREATE MATERIALIZED VIEW {s}
- \\TO {s}
- , .{ self.name, self.target_table });
-
- if (self.cluster) |cluster| {
- try query_buf.writer().print(" ON CLUSTER {s}", .{cluster});
- }
-
- if (self.populate) {
- try query_buf.appendSlice(" POPULATE");
- }
-
- try query_buf.writer().print(" AS {s}", .{self.query});
-
- try client.query(query_buf.items);
- }
-
- pub fn drop(self: *MaterializedView, client: *ClickHouseClient) !void {
- var query_buf = std.ArrayList(u8).init(self.allocator);
- defer query_buf.deinit();
-
- try query_buf.writer().print("DROP VIEW IF EXISTS {s}", .{self.name});
- if (self.cluster) |cluster| {
- try query_buf.writer().print(" ON CLUSTER {s}", .{cluster});
- }
-
- try client.query(query_buf.items);
- }
-
- pub fn refresh(self: *MaterializedView, client: *ClickHouseClient) !void {
- var query_buf = std.ArrayList(u8).init(self.allocator);
- defer query_buf.deinit();
-
- try query_buf.writer().print("ALTER VIEW {s} REFRESH", .{self.name});
- if (self.cluster) |cluster| {
- try query_buf.writer().print(" ON CLUSTER {s}", .{cluster});
- }
-
- try client.query(query_buf.items);
- }
-};
-
-pub const View = struct {
- name: []const u8,
- query: []const u8,
- cluster: ?[]const u8,
- allocator: std.mem.Allocator,
-
- pub fn init(
- allocator: std.mem.Allocator,
- name: []const u8,
- query: []const u8,
- ) !*View {
- var view = try allocator.create(View);
- view.* = .{
- .name = try allocator.dupe(u8, name),
- .query = try allocator.dupe(u8, query),
- .cluster = null,
- .allocator = allocator,
- };
- return view;
- }
-
- pub fn deinit(self: *View) void {
- self.allocator.free(self.name);
- self.allocator.free(self.query);
- if (self.cluster) |cluster| {
- self.allocator.free(cluster);
- }
- self.allocator.destroy(self);
- }
-
- pub fn setCluster(self: *View, cluster: []const u8) !void {
- if (self.cluster) |old_cluster| {
- self.allocator.free(old_cluster);
- }
- self.cluster = try self.allocator.dupe(u8, cluster);
- }
-
- pub fn create(self: *View, client: *ClickHouseClient) !void {
- var query_buf = std.ArrayList(u8).init(self.allocator);
- defer query_buf.deinit();
-
- try query_buf.writer().print("CREATE VIEW {s}", .{self.name});
- if (self.cluster) |cluster| {
- try query_buf.writer().print(" ON CLUSTER {s}", .{cluster});
- }
- try query_buf.writer().print(" AS {s}", .{self.query});
-
- try client.query(query_buf.items);
- }
-
- pub fn drop(self: *View, client: *ClickHouseClient) !void {
- var query_buf = std.ArrayList(u8).init(self.allocator);
- defer query_buf.deinit();
-
- try query_buf.writer().print("DROP VIEW IF EXISTS {s}", .{self.name});
- if (self.cluster) |cluster| {
- try query_buf.writer().print(" ON CLUSTER {s}", .{cluster});
- }
-
- try client.query(query_buf.items);
- }
-};
\ No newline at end of file
diff --git a/src/addendum.zig b/src/addendum.zig
index e16a6da..385ca2f 100644
--- a/src/addendum.zig
+++ b/src/addendum.zig
@@ -62,14 +62,12 @@ pub fn writeClientAddendum(
/// True if the server's reported send-side chunking choice means
/// server→client data is being framed in chunks.
pub fn isChunkedSendEngaged(server_send_chunked_srv: []const u8) bool {
- return std.mem.eql(u8, server_send_chunked_srv, "chunked")
- or std.mem.eql(u8, server_send_chunked_srv, "chunked_optional");
+ return std.mem.eql(u8, server_send_chunked_srv, "chunked") or std.mem.eql(u8, server_send_chunked_srv, "chunked_optional");
}
/// Same, for client→server direction.
pub fn isChunkedRecvEngaged(server_recv_chunked_srv: []const u8) bool {
- return std.mem.eql(u8, server_recv_chunked_srv, "chunked")
- or std.mem.eql(u8, server_recv_chunked_srv, "chunked_optional");
+ return std.mem.eql(u8, server_recv_chunked_srv, "chunked") or std.mem.eql(u8, server_recv_chunked_srv, "chunked_optional");
}
// --- tests -----------------------------------------------------------------
diff --git a/src/cherror.zig b/src/cherror.zig
index 0c038c6..243df50 100644
--- a/src/cherror.zig
+++ b/src/cherror.zig
@@ -32,8 +32,8 @@ pub const ClientError = error{
TypeMismatch,
/// Compression header bad, decompression failed, or checksum mismatch.
CompressionError,
- /// Operation cancelled by caller or by the Io runtime.
- Cancelled,
+ /// Operation canceled through `std.Io` (Future/Group cancel).
+ Canceled,
/// Authentication rejected (wrong user/password, db not permitted).
AuthenticationFailed,
/// Allocator returned OutOfMemory mid-decode. Re-exported so callers
diff --git a/src/cityhash.zig b/src/cityhash.zig
index ef77422..da25dc7 100644
--- a/src/cityhash.zig
+++ b/src/cityhash.zig
@@ -285,9 +285,14 @@ test "cityhash128 of 'a'" {
}
test "cityhash128 of long input is deterministic" {
- const data = "The quick brown fox jumps over the lazy dog. " ** 8;
- const h1 = cityhash128(data);
- const h2 = cityhash128(data);
+ const sentence = "The quick brown fox jumps over the lazy dog. ";
+ const data = comptime blk: {
+ var buf: [sentence.len * 8]u8 = undefined;
+ for (0..8) |i| @memcpy(buf[i * sentence.len ..][0..sentence.len], sentence);
+ break :blk buf;
+ };
+ const h1 = cityhash128(&data);
+ const h2 = cityhash128(&data);
try testing.expectEqual(h1.low, h2.low);
try testing.expectEqual(h1.high, h2.high);
}
diff --git a/src/client.zig b/src/client.zig
index 503cc75..e6958a0 100644
--- a/src/client.zig
+++ b/src/client.zig
@@ -16,7 +16,7 @@
//!
//! `Client` is non-copyable (decision #12). Constructors return
//! `*Client` allocated from `config.control_allocator`. Caller pattern:
-//! const client = try Client.connectTcp(cfg, io, null, null);
+//! const client = try Client.connectTcp(cfg, io, null);
//! defer client.close(); // single-call; frees the Client struct
//!
//! THREAD-SAFETY: a single `Client` is NOT thread-safe. State machine
@@ -26,10 +26,20 @@
//! the same `Client` are undefined behaviour. For multi-threaded
//! workloads, use one `Client` per thread or the built-in Pool.
//!
-//! Cancellation note: `*const std.atomic.Value(bool)` is the right
-//! primitive for our sync API. Zig 0.16's `Future.cancel` is tied to
-//! async task cancellation and would require restructuring every public
-//! method around `io.async`. The atomic-bool stays.
+//! ASYNC + CANCELLATION: every blocking step goes through the `std.Io`
+//! passed at connect time, so the same API runs on `Io.Threaded` (one OS
+//! thread per blocking call) or on an evented `Io` (fibers over io_uring /
+//! kqueue). Run calls concurrently with `io.async` / `Io.Group`, one
+//! Client per in-flight call (or use `Pool`).
+//!
+//! Cancellation is `Io` cancellation: `future.cancel(io)` or
+//! `group.cancel(io)` makes the pending socket operation return
+//! `error.Canceled`, which every Client method propagates unchanged.
+//! If a query is canceled while the client is waiting for the next
+//! server packet, the client sends a protocol `Cancel` packet and drains
+//! the server's reply (shielded from further cancellation, bounded by
+//! `Config.cancel_drain_timeout_ms`), so the connection stays reusable.
+//! A cancel that lands mid-packet leaves the connection broken.
const std = @import("std");
const protocol = @import("protocol.zig");
@@ -58,6 +68,8 @@ pub const Event = enum {
ping_sent,
pong_received,
query_sent,
+ /// A canceled query sent a protocol `Cancel` packet to the server.
+ query_cancel_sent,
closing,
closed,
};
@@ -67,7 +79,10 @@ pub const QueryError = error{
ProtocolError,
ReadTimeout,
WriteTimeout,
- Cancelled,
+ /// The calling task was canceled via `std.Io` (`Future.cancel`,
+ /// `Group.cancel`). Check `Client.isReusable()` to see whether the
+ /// in-flight query was cleanly aborted.
+ Canceled,
OutOfMemory,
ClientNotReady,
};
@@ -111,8 +126,8 @@ pub const ConnectError = error{
ReadTimeout,
/// Write on an established socket exceeded `write_timeout_ms`.
WriteTimeout,
- /// Caller-provided cancellation token observed `true`.
- Cancelled,
+ /// The calling task was canceled via `std.Io`.
+ Canceled,
OutOfMemory,
};
@@ -144,6 +159,14 @@ pub const Config = struct {
dial_timeout_ms: u32 = 30_000,
read_timeout_ms: u32 = 60_000,
write_timeout_ms: u32 = 30_000,
+ /// Read budget for draining the server's reply after a canceled
+ /// query sends a protocol `Cancel` packet. If the server does not
+ /// finish within this window the connection is marked broken.
+ cancel_drain_timeout_ms: u32 = 5_000,
+ /// Disable Nagle's algorithm on the built-in TCP transport. Leave on
+ /// unless you have a specific reason: with Nagle, small trailing
+ /// packets stall on delayed ACKs (~40ms each).
+ tcp_no_delay: bool = true,
/// Per-client default settings forwarded by query/insert paths.
settings: ?*const std.StringHashMapUnmanaged([]const u8) = null,
@@ -187,16 +210,15 @@ pub const Client = struct {
pub fn connectTcp(
config: Config,
io: std.Io,
- cancel: ?*const std.atomic.Value(bool),
diag: ?*Diagnostics,
) ConnectError!*Client {
emit(config, .connecting);
- if (cancellationRequested(cancel)) return error.Cancelled;
const tcp = transport_mod.TcpTransport.connect(config.control_allocator, io, config.host, config.port, .{
.read_buffer_size = config.read_buffer_size,
.write_buffer_size = config.write_buffer_size,
.dial_timeout_ms = config.dial_timeout_ms,
+ .no_delay = config.tcp_no_delay,
}) catch |e| return mapTcpConnectError(e);
errdefer {
tcp.deinit();
@@ -206,7 +228,7 @@ pub const Client = struct {
tcp.transport().setReadTimeout(config.read_timeout_ms) catch {};
tcp.transport().setWriteTimeout(config.write_timeout_ms) catch {};
- const client = try fromTransport(config, io, tcp.transport(), cancel, diag);
+ const client = try fromTransport(config, io, tcp.transport(), diag);
client.transport_owned = tcp;
return client;
}
@@ -217,7 +239,6 @@ pub const Client = struct {
config: Config,
io: std.Io,
transport: transport_mod.Transport,
- cancel: ?*const std.atomic.Value(bool),
diag: ?*Diagnostics,
) ConnectError!*Client {
const client = config.control_allocator.create(Client) catch return error.OutOfMemory;
@@ -238,12 +259,6 @@ pub const Client = struct {
.compression_method = config.compression_method,
};
- if (cancellationRequested(cancel)) {
- client.is_broken = true;
- client.state = .broken;
- return error.Cancelled;
- }
-
// --- Hello write + flush ---
emit(config, .hello_sent);
hello.writeClientHello(transport.writer(), config.database, config.username, config.password, config.client_name) catch |e| {
@@ -257,12 +272,6 @@ pub const Client = struct {
return mapWriteError(transport, e);
};
- if (cancellationRequested(cancel)) {
- client.is_broken = true;
- client.state = .broken;
- return error.Cancelled;
- }
-
// --- ServerHello / Exception read ---
const result = hello.readHelloResult(transport.reader(), config.control_allocator) catch |e| {
client.is_broken = true;
@@ -284,22 +293,13 @@ pub const Client = struct {
// ergonomic catch-blocks. Modern CH (22.x+) collapsed
// WRONG_PASSWORD/REQUIRED_PASSWORD into AUTHENTICATION_FAILED;
// accept all three for cross-version compatibility.
- const is_auth = exc.code == cherror.Code.AUTHENTICATION_FAILED
- or exc.code == cherror.Code.REQUIRED_PASSWORD
- or exc.code == cherror.Code.WRONG_PASSWORD
- or exc.code == cherror.Code.UNKNOWN_USER;
+ const is_auth = exc.code == cherror.Code.AUTHENTICATION_FAILED or exc.code == cherror.Code.REQUIRED_PASSWORD or exc.code == cherror.Code.WRONG_PASSWORD or exc.code == cherror.Code.UNKNOWN_USER;
return if (is_auth) error.AuthenticationFailed else error.ServerExceptionDuringHello;
},
}
emit(config, .hello_received);
errdefer client.server_info.deinit();
- if (cancellationRequested(cancel)) {
- client.is_broken = true;
- client.state = .broken;
- return error.Cancelled;
- }
-
// --- Addendum write + flush (no-op for pre-54_458 servers) ---
// Use the negotiated revision, NOT the server's reported revision —
// we must only write addendum fields the server expects given OUR
@@ -353,12 +353,9 @@ pub const Client = struct {
}
/// Health check. Sends Ping, expects Pong. Updates last_used_at_ms.
- pub fn ping(
- self: *Client,
- cancel: ?*const std.atomic.Value(bool),
- ) ConnectError!void {
+ pub fn ping(self: *Client) ConnectError!void {
if (self.state != .ready) return error.ConnectionFailed;
- if (cancellationRequested(cancel)) return error.Cancelled;
+ self.io.checkCancel() catch return error.Canceled;
self.state = .busy;
wire.writeClientPacketId(self.transport.writer(), .Ping) catch |e| {
@@ -373,12 +370,6 @@ pub const Client = struct {
};
emit(self.config, .ping_sent);
- if (cancellationRequested(cancel)) {
- self.is_broken = true;
- self.state = .broken;
- return error.Cancelled;
- }
-
const pkt = wire.readServerPacketId(self.transport.reader()) catch |e| {
self.is_broken = true;
self.state = .broken;
@@ -409,11 +400,12 @@ pub const Client = struct {
pub fn sendQuery(
self: *Client,
query_text: []const u8,
- cancel: ?*const std.atomic.Value(bool),
opts_in: query_mod.QueryOptions,
) QueryError!void {
if (self.state != .ready) return error.ClientNotReady;
- if (cancellationRequested(cancel)) return error.Cancelled;
+ // Observe a pending cancel before any byte hits the wire, so a
+ // task canceled before it started leaves the connection usable.
+ self.io.checkCancel() catch return error.Canceled;
self.state = .busy;
// If caller didn't override, fall back to the client-wide setting.
@@ -513,11 +505,10 @@ pub const Client = struct {
self: *Client,
query_text: []const u8,
query_allocator: std.mem.Allocator,
- cancel: ?*const std.atomic.Value(bool),
opts: query_mod.QueryOptions,
) QueryError!result_stream_mod.ResultStream {
const effective_compression: protocol.CompressionEnabled = if (opts.compression == .Enable or self.compression == .Enable) .Enable else .Disable;
- try self.sendQuery(query_text, cancel, opts);
+ try self.sendQuery(query_text, opts);
return .{
.reader = self.transport.reader(),
.allocator = query_allocator,
@@ -529,6 +520,7 @@ pub const Client = struct {
.is_broken = &self.is_broken,
.set_ready = setReadyImpl,
.set_broken = setBrokenImpl,
+ .abort = abortImpl,
},
};
}
@@ -548,10 +540,9 @@ pub const Client = struct {
num_rows: u64,
columns: []const block_mod.InsertColumn,
query_allocator: std.mem.Allocator,
- cancel: ?*const std.atomic.Value(bool),
opts: query_mod.QueryOptions,
) QueryError!void {
- try self.sendQuery(query_text, cancel, opts);
+ try self.sendQuery(query_text, opts);
const negotiated = self.server_info.negotiated(protocol.CLIENT_REVISION);
const effective_compression: protocol.CompressionEnabled = if (opts.compression == .Enable or self.compression == .Enable) .Enable else .Disable;
const effective_compression_method: compression_mod.WriteMethod = if (opts.compression_method != .lz4) opts.compression_method else self.compression_method;
@@ -565,9 +556,16 @@ pub const Client = struct {
var schema_seen = false;
while (!schema_seen) {
const pkt = wire.readServerPacketId(self.transport.reader()) catch |e| {
+ const mapped = mapReadErrQuery(self.transport, e);
+ // Canceled while waiting at a packet boundary: abort the
+ // INSERT server-side and keep the connection if we can.
+ if (mapped == error.Canceled and self.abortQuery(query_allocator, effective_compression)) {
+ self.state = .ready;
+ return error.Canceled;
+ }
self.is_broken = true;
self.state = .broken;
- return mapReadErrQuery(self.transport, e);
+ return mapped;
};
switch (pkt) {
.Data => {
@@ -718,6 +716,7 @@ pub const Client = struct {
.is_broken = &self.is_broken,
.set_ready = setReadyImpl,
.set_broken = setBrokenImpl,
+ .abort = abortImpl,
},
};
while (try mapStreamErr(stream.next())) |packet| switch (packet) {
@@ -731,15 +730,67 @@ pub const Client = struct {
.progress, .profile_info => {},
};
}
+
+ /// Abort the in-flight query: send a protocol `Cancel` packet and
+ /// drain the server's reply up to `EndOfStream` / `Exception`.
+ /// Runs with cancellation blocked so a pending cancel request cannot
+ /// interrupt the cleanup itself; reads are bounded by
+ /// `Config.cancel_drain_timeout_ms` (owned TCP transports only).
+ /// Returns true when the connection is back in a clean state. The
+ /// caller decides the resulting `state`.
+ fn abortQuery(
+ self: *Client,
+ allocator: std.mem.Allocator,
+ compression: protocol.CompressionEnabled,
+ ) bool {
+ const io = self.io;
+ const prev_protection = io.swapCancelProtection(.blocked);
+ defer _ = io.swapCancelProtection(prev_protection);
+
+ wire.writeClientPacketId(self.transport.writer(), .Cancel) catch return false;
+ self.transport.writer().flush() catch return false;
+ emit(self.config, .query_cancel_sent);
+
+ const owns_timeouts = self.transport_owned != null;
+ if (owns_timeouts) self.transport.setReadTimeout(self.config.cancel_drain_timeout_ms) catch {};
+ defer if (owns_timeouts) self.transport.setReadTimeout(self.config.read_timeout_ms) catch {};
+
+ var drain: result_stream_mod.ResultStream = .{
+ .reader = self.transport.reader(),
+ .allocator = allocator,
+ .server_revision = self.server_info.negotiated(protocol.CLIENT_REVISION),
+ .compression = compression,
+ .transport = self.transport,
+ };
+ while (true) {
+ const packet = (drain.next() catch return false) orelse return false;
+ switch (packet) {
+ .end_of_stream => return true,
+ .exception => |e| {
+ // Usually QUERY_WAS_CANCELLED; either way the
+ // server has terminated the query.
+ e.deinit();
+ return true;
+ },
+ else => packet.deinit(allocator),
+ }
+ }
+ }
};
+fn abortImpl(state: *anyopaque, allocator: std.mem.Allocator, compression: protocol.CompressionEnabled) bool {
+ const c: *Client = @ptrCast(@alignCast(state));
+ return c.abortQuery(allocator, compression);
+}
+
fn mapReadErrQuery(transport: ?transport_mod.Transport, e: anyerror) QueryError {
if (readTimedOut(transport, e)) return error.ReadTimeout;
+ if (readCanceled(transport, e)) return error.Canceled;
return switch (e) {
error.ReadFailed, error.EndOfStream => error.ConnectionFailed,
error.Timeout => error.ReadTimeout,
error.OutOfMemory => error.OutOfMemory,
- error.Canceled, error.Cancelled => error.Cancelled,
+ error.Canceled => error.Canceled,
else => error.ProtocolError,
};
}
@@ -748,6 +799,7 @@ fn mapStreamErr(r: anytype) QueryError!@typeInfo(@TypeOf(r)).error_union.payload
return r catch |e| switch (e) {
error.OutOfMemory => error.OutOfMemory,
error.ReadTimeout => error.ReadTimeout,
+ error.Canceled => error.Canceled,
else => error.ProtocolError,
};
}
@@ -772,29 +824,26 @@ inline fn emit(cfg: Config, event: Event) void {
}
}
-inline fn cancellationRequested(cancel: ?*const std.atomic.Value(bool)) bool {
- if (cancel) |c| return c.load(.acquire);
- return false;
-}
-
fn mapWriteError(transport: ?transport_mod.Transport, e: anyerror) ConnectError {
if (writeTimedOut(transport, e)) return error.WriteTimeout;
+ if (writeCanceled(transport, e)) return error.Canceled;
return switch (e) {
error.WriteFailed => error.ConnectionFailed,
error.Timeout => error.WriteTimeout,
error.OutOfMemory => error.OutOfMemory,
- error.Canceled, error.Cancelled => error.Cancelled,
+ error.Canceled => error.Canceled,
else => error.ConnectionFailed,
};
}
fn mapReadError(transport: ?transport_mod.Transport, e: anyerror) ConnectError {
if (readTimedOut(transport, e)) return error.ReadTimeout;
+ if (readCanceled(transport, e)) return error.Canceled;
return switch (e) {
error.ReadFailed, error.EndOfStream => error.ConnectionFailed,
error.Timeout => error.ReadTimeout,
error.OutOfMemory => error.OutOfMemory,
- error.Canceled, error.Cancelled => error.Cancelled,
+ error.Canceled => error.Canceled,
error.OverlongVarint,
error.VarintOverflow,
error.UnknownServerPacket,
@@ -808,11 +857,12 @@ fn mapReadError(transport: ?transport_mod.Transport, e: anyerror) ConnectError {
fn mapWriteErrQuery(transport: ?transport_mod.Transport, e: anyerror) QueryError {
if (writeTimedOut(transport, e)) return error.WriteTimeout;
+ if (writeCanceled(transport, e)) return error.Canceled;
return switch (e) {
error.WriteFailed => error.ConnectionFailed,
error.Timeout => error.WriteTimeout,
error.OutOfMemory => error.OutOfMemory,
- error.Canceled, error.Cancelled => error.Cancelled,
+ error.Canceled => error.Canceled,
else => error.ConnectionFailed,
};
}
@@ -833,6 +883,24 @@ fn writeTimedOut(transport: ?transport_mod.Transport, e: anyerror) bool {
return err == error.Timeout;
}
+/// The transport reports `error.ReadFailed` / `error.WriteFailed` through
+/// the `std.Io.Reader`/`Writer` interface and keeps the real cause on the
+/// side. Recover `error.Canceled` from there so cancellation is reported
+/// as such instead of as a connection failure.
+fn readCanceled(transport: ?transport_mod.Transport, e: anyerror) bool {
+ if (e == error.Canceled) return true;
+ if (e != error.ReadFailed) return false;
+ const t = transport orelse return false;
+ return (t.lastReadError() orelse return false) == error.Canceled;
+}
+
+fn writeCanceled(transport: ?transport_mod.Transport, e: anyerror) bool {
+ if (e == error.Canceled) return true;
+ if (e != error.WriteFailed) return false;
+ const t = transport orelse return false;
+ return (t.lastWriteError() orelse return false) == error.Canceled;
+}
+
fn mapTcpConnectError(e: transport_mod.ConnectError) ConnectError {
return switch (e) {
error.ConnectionTimeout => error.ConnectTimeout,
@@ -841,7 +909,7 @@ fn mapTcpConnectError(e: transport_mod.ConnectError) ConnectError {
error.HostResolutionFailed, error.InvalidHost => error.DnsFailure,
error.OutOfMemory => error.OutOfMemory,
error.ConcurrencyUnavailable => error.ConnectionFailed,
- error.Canceled => error.Cancelled,
+ error.Canceled => error.Canceled,
};
}
@@ -890,12 +958,11 @@ test "timeout errors map to public connect/read/write variants" {
test "Event enum covers handshake + ping + close lifecycle" {
// Compile-time check that the values listed in the plan are present.
const _e = [_]Event{
- .connecting, .connected,
- .hello_sent, .hello_received,
- .addendum_sent, .server_exception_received,
- .ping_sent, .pong_received,
- .closing, .closed,
+ .connecting, .connected,
+ .hello_sent, .hello_received,
+ .addendum_sent, .server_exception_received,
+ .ping_sent, .pong_received,
+ .closing, .closed,
};
_ = _e;
}
-
diff --git a/src/client_info.zig b/src/client_info.zig
index b67724d..12e13d5 100644
--- a/src/client_info.zig
+++ b/src/client_info.zig
@@ -67,7 +67,7 @@ pub fn writeClientInfo(
info: ClientInfo,
server_revision: u64,
) std.Io.Writer.Error!void {
- try writer.writeByte(@intFromEnum(info.query_kind));
+ try writer.writeByte(@backingInt(info.query_kind));
if (info.query_kind == .NoQuery) return;
try wire.writeStringBinary(writer, info.initial_user);
@@ -78,7 +78,7 @@ pub fn writeClientInfo(
try writer.writeInt(u64, info.initial_query_start_time_microseconds, .little);
}
- try writer.writeByte(@intFromEnum(info.interface));
+ try writer.writeByte(@backingInt(info.interface));
// Interface == TCP branch: the only interface this client emits.
try wire.writeStringBinary(writer, info.os_user);
try wire.writeStringBinary(writer, info.client_hostname);
diff --git a/src/column.zig b/src/column.zig
index 76a2c5b..c25392a 100644
--- a/src/column.zig
+++ b/src/column.zig
@@ -22,9 +22,20 @@ pub const MAX_COLUMN_BYTES: usize = 1024 * 1024 * 1024;
/// Identifies a primitive (non-wrapped) column type.
pub const TypeId = enum {
- UInt8, UInt16, UInt32, UInt64, UInt128, UInt256,
- Int8, Int16, Int32, Int64, Int128, Int256,
- Float32, Float64,
+ UInt8,
+ UInt16,
+ UInt32,
+ UInt64,
+ UInt128,
+ UInt256,
+ Int8,
+ Int16,
+ Int32,
+ Int64,
+ Int128,
+ Int256,
+ Float32,
+ Float64,
String,
};
@@ -260,7 +271,7 @@ pub fn typeIdFromName(type_name: []const u8) ?TypeId {
// P <= 18 → Int64 (Decimal64)
// P <= 38 → Int128 (Decimal128)
// P <= 76 → Int256 (Decimal256)
- const inside = type_name[8..type_name.len - 1];
+ const inside = type_name[8 .. type_name.len - 1];
const comma = std.mem.indexOfScalar(u8, inside, ',') orelse return null;
const p_str = std.mem.trim(u8, inside[0..comma], " ");
const p = std.fmt.parseInt(u8, p_str, 10) catch return null;
@@ -394,10 +405,7 @@ pub fn readColumn(
}
pub fn hasCustomSerialization(type_name: []const u8) bool {
- return std.mem.eql(u8, type_name, "JSON")
- or std.mem.startsWith(u8, type_name, "JSON(")
- or std.mem.eql(u8, type_name, "Dynamic")
- or extractSparseInner(type_name) != null;
+ return std.mem.eql(u8, type_name, "JSON") or std.mem.startsWith(u8, type_name, "JSON(") or std.mem.eql(u8, type_name, "Dynamic") or extractSparseInner(type_name) != null;
}
pub fn readCustomColumn(
@@ -669,7 +677,9 @@ pub fn extractMapKV(type_name: []const u8) ?MapKV {
while (i < inside.len) : (i += 1) {
switch (inside[i]) {
'(' => depth += 1,
- ')' => if (depth > 0) { depth -= 1; },
+ ')' => if (depth > 0) {
+ depth -= 1;
+ },
',' => if (depth == 0) {
const k = std.mem.trim(u8, inside[0..i], " ");
const v = std.mem.trim(u8, inside[i + 1 ..], " ");
@@ -1342,7 +1352,7 @@ test "Array(UInt32) round-trips" {
test "FixedString(4) round-trips" {
var buf: [32]u8 = undefined;
var w: std.Io.Writer = .fixed(&buf);
- var data = [_]u8{ 'a','b','c','d', 'e','f','g','h', 'i','j','k','l' };
+ var data = [_]u8{ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l' };
const col_in: Column = .{ .FixedString = .{ .width = 4, .data = &data } };
try writeColumn(&w, col_in);
@@ -1357,7 +1367,7 @@ test "FixedString(4) round-trips" {
test "UUID round-trips" {
var buf: [64]u8 = undefined;
var w: std.Io.Writer = .fixed(&buf);
- var rows: [2][16]u8 = .{ .{1}**16, .{2,2,2,2, 2,2,2,2, 2,2,2,2, 2,2,2,2} };
+ var rows: [2][16]u8 = .{ @splat(1), @splat(2) };
const col_in: Column = .{ .UUID = &rows };
try writeColumn(&w, col_in);
@@ -1881,7 +1891,7 @@ test "writeLowCardinality(FixedString(4)) round-trips" {
const ally = testing.allocator;
var buf: [1024]u8 = undefined;
var w: std.Io.Writer = .fixed(&buf);
- var data = [_]u8{ 'a','a','a','a', 'b','b','b','b', 'a','a','a','a' };
+ var data = [_]u8{ 'a', 'a', 'a', 'a', 'b', 'b', 'b', 'b', 'a', 'a', 'a', 'a' };
const col_in: Column = .{ .FixedString = .{ .width = 4, .data = &data } };
try writeLowCardinality(&w, ally, "FixedString(4)", col_in, 3);
diff --git a/src/compression.zig b/src/compression.zig
index 419bdb3..1ed14f5 100644
--- a/src/compression.zig
+++ b/src/compression.zig
@@ -144,7 +144,7 @@ pub fn writeFrameLz4(
const compressed_size: u32 = @intCast(HEADER_TAIL_BYTES + enc_len);
const decompressed_size: u32 = @intCast(data.len);
- hash_buf[0] = @intFromEnum(Method.lz4);
+ hash_buf[0] = @backingInt(Method.lz4);
std.mem.writeInt(u32, hash_buf[1..5], compressed_size, .little);
std.mem.writeInt(u32, hash_buf[5..9], decompressed_size, .little);
const used = hash_buf[0 .. HEADER_TAIL_BYTES + enc_len];
@@ -173,7 +173,7 @@ pub fn writeFrameZstd(
const decompressed_size: u32 = @intCast(data.len);
var hash_buf = try allocator.alloc(u8, HEADER_TAIL_BYTES + raw_payload.len);
defer allocator.free(hash_buf);
- hash_buf[0] = @intFromEnum(Method.zstd);
+ hash_buf[0] = @backingInt(Method.zstd);
std.mem.writeInt(u32, hash_buf[1..5], compressed_size, .little);
std.mem.writeInt(u32, hash_buf[5..9], decompressed_size, .little);
@memcpy(hash_buf[HEADER_TAIL_BYTES..], raw_payload);
@@ -239,7 +239,7 @@ fn writeRawFrameForTest(
) !void {
var hash_buf: std.Io.Writer.Allocating = .init(testing.allocator);
defer hash_buf.deinit();
- try hash_buf.writer.writeByte(@intFromEnum(method));
+ try hash_buf.writer.writeByte(@backingInt(method));
try hash_buf.writer.writeInt(u32, @intCast(HEADER_TAIL_BYTES + compressed_payload.len), .little);
try hash_buf.writer.writeInt(u32, decompressed_size, .little);
try hash_buf.writer.writeAll(compressed_payload);
@@ -340,7 +340,7 @@ test "readFrame rejects none frame with mismatched payload and decompressed size
test "readFrame rejects decompressed size over cap before allocation" {
var tail: [9]u8 = undefined;
- tail[0] = @intFromEnum(Method.lz4);
+ tail[0] = @backingInt(Method.lz4);
std.mem.writeInt(u32, tail[1..5], 9, .little);
std.mem.writeInt(u32, tail[5..9], @intCast(MAX_DECOMPRESSED_FRAME_BYTES + 1), .little);
const sum = cityhash.cityhash128(&tail);
diff --git a/src/exception.zig b/src/exception.zig
index d3d5e20..2fddc53 100644
--- a/src/exception.zig
+++ b/src/exception.zig
@@ -44,11 +44,7 @@ pub const Error = error{
pub fn readException(
reader: *std.Io.Reader,
allocator: std.mem.Allocator,
-) (std.Io.Reader.Error
- || std.Io.Reader.ReadAllocError
- || @import("varint.zig").Error
- || wire.Error
- || Error)!cherror.ServerError {
+) (std.Io.Reader.Error || std.Io.Reader.ReadAllocError || @import("varint.zig").Error || wire.Error || Error)!cherror.ServerError {
const code_i32 = try reader.takeInt(i32, .little);
const name = try wire.readStringOwned(reader, allocator, MAX_EXCEPTION_STRING);
diff --git a/src/hello.zig b/src/hello.zig
index de08900..8619ba5 100644
--- a/src/hello.zig
+++ b/src/hello.zig
@@ -327,7 +327,7 @@ test "readHelloResult routes Exception packet to server_exception" {
var buf: [256]u8 = undefined;
var w: std.Io.Writer = .fixed(&buf);
// Exception packet body: code, name, message, stack, has_nested
- try varint.writeVarUInt(&w, @intFromEnum(protocol.ServerPacket.Exception));
+ try varint.writeVarUInt(&w, @backingInt(protocol.ServerPacket.Exception));
try w.writeInt(i32, 194, .little);
try wire.writeStringBinary(&w, "DB::Exception");
try wire.writeStringBinary(&w, "Required password");
diff --git a/src/main.zig b/src/main.zig
index 3979e3c..e4c1862 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -1,8 +1,11 @@
-//! clickzig - native-protocol ClickHouse client for Zig 0.16+
+//! clickzig - native-protocol ClickHouse client for Zig 0.17+
//!
-//! Status: v0.16.0. Sync TCP client targeting the ClickHouse
-//! native protocol (port 9000), with query streaming, Native INSERT,
+//! Status: v0.17.0. `std.Io`-native client for the ClickHouse native
+//! protocol (port 9000), with query streaming, Native INSERT,
//! compression, pooling, TLS transport support, and DSN parsing.
+//! Every blocking step goes through the caller's `std.Io`, so the same
+//! API is synchronous on `Io.Threaded` and asynchronous on an evented
+//! `Io`; cancellation is `Io` cancellation (`Future.cancel`).
//!
//! Usage sketch:
//! var threaded: std.Io.Threaded = .init(gpa, .{});
@@ -15,10 +18,10 @@
//! .write_buffer_size = 4 * 1024,
//! .username = "default",
//! .password = "test",
-//! }, io, null, null);
+//! }, io, null);
//! defer client.close();
//!
-//! try client.ping(null);
+//! try client.ping();
const std = @import("std");
@@ -62,6 +65,7 @@ pub const Config = client_mod.Config;
pub const Event = client_mod.Event;
pub const Diagnostics = client_mod.Diagnostics;
pub const ConnectError = client_mod.ConnectError;
+pub const QueryError = client_mod.QueryError;
pub const State = client_mod.State;
pub const ServerInfo = client_mod.ServerInfo;
@@ -74,7 +78,7 @@ pub const TcpOptions = transport_mod.TcpOptions;
/// per-query overrides attach on QueryOptions.
pub const SettingsMap = std.StringHashMapUnmanaged([]const u8);
-pub const VERSION = "0.16.0";
+pub const VERSION = "0.17.0";
test {
std.testing.refAllDecls(@This());
diff --git a/src/pool.zig b/src/pool.zig
index 7bf771d..7896219 100644
--- a/src/pool.zig
+++ b/src/pool.zig
@@ -8,9 +8,9 @@
//! Lifecycle:
//! var pool = try Pool.init(allocator, io, config, .{ .max_size = 8 });
//! defer pool.deinit();
-//! const client = try pool.acquire(null);
+//! const client = try pool.acquire();
//! defer pool.release(client);
-//! try client.ping(null);
+//! try client.ping();
//!
//! Behaviour:
//! - Lazy creation: dial only when an acquire can't be satisfied
@@ -21,11 +21,14 @@
//! - Stale checking: optional `max_lifetime_ms` retires Clients that
//! have lived longer than the budget on next release.
//!
-//! Threading model:
-//! - The pool uses a `std.Thread.Mutex` to guard the idle/in-use
-//! bookkeeping. Acquire and release are O(1) amortised.
-//! - Concurrent acquires beyond max_size block on a `Condition` until
-//! someone releases.
+//! Concurrency model:
+//! - The pool uses `std.Io.Mutex` / `std.Io.Condition`, so it is safe
+//! to share between OS threads (`Io.Threaded`) and between fibers
+//! on an evented `Io`. Acquire and release are O(1) amortised.
+//! - Concurrent acquires beyond max_size wait on the `Condition` until
+//! someone releases. That wait (and dialing a new Client) is a
+//! cancellation point: canceling the waiting task makes `acquire`
+//! return `error.Canceled` without leaking a slot.
//!
//! This file exceeds 100 lines because the lifecycle + bookkeeping +
//! state machine + tests live together. Splitting them would obscure
@@ -42,7 +45,6 @@ pub const Options = struct {
pub const Error = error{
PoolClosed,
- AcquireCancelled,
} || client_mod.ConnectError;
pub const Pool = struct {
@@ -87,20 +89,16 @@ pub const Pool = struct {
self.allocator.destroy(self);
}
- /// Acquire a Client from the pool. Blocks if all max_size Clients
- /// are currently in use (until one is released). Returns the
- /// caller's cancellation error if the cancel token flips while
- /// waiting.
- pub fn acquire(
- self: *Pool,
- cancel: ?*const std.atomic.Value(bool),
- ) Error!*client_mod.Client {
- self.mutex.lockUncancelable(self.io);
+ /// Acquire a Client from the pool. Waits if all max_size Clients
+ /// are currently in use (until one is released). Returns
+ /// `error.Canceled` if the calling task is canceled while waiting
+ /// or dialing.
+ pub fn acquire(self: *Pool) Error!*client_mod.Client {
+ try self.mutex.lock(self.io);
defer self.mutex.unlock(self.io);
while (true) {
if (self.closed) return error.PoolClosed;
- if (cancellationRequested(cancel)) return error.AcquireCancelled;
// Grab an idle Client first, validating it's still good.
while (self.idle.pop()) |c| {
@@ -116,7 +114,7 @@ pub const Pool = struct {
if (self.live_count < self.opts.max_size) {
self.live_count += 1;
self.mutex.unlock(self.io);
- const c = client_mod.Client.connectTcp(self.config, self.io, cancel, null) catch |e| {
+ const c = client_mod.Client.connectTcp(self.config, self.io, null) catch |e| {
self.mutex.lockUncancelable(self.io);
self.live_count -= 1;
self.cond.signal(self.io);
@@ -126,8 +124,9 @@ pub const Pool = struct {
return c;
}
- // At cap; wait for a release.
- self.cond.waitUncancelable(self.io, &self.mutex);
+ // At cap; wait for a release. `wait` re-acquires the mutex
+ // before returning, including on error.Canceled.
+ try self.cond.wait(self.io, &self.mutex);
}
}
@@ -180,11 +179,6 @@ pub const Pool = struct {
}
};
-fn cancellationRequested(cancel: ?*const std.atomic.Value(bool)) bool {
- if (cancel) |c| return c.load(.acquire);
- return false;
-}
-
const testing = std.testing;
test "Pool deinit on empty pool is safe" {
diff --git a/src/protocol.zig b/src/protocol.zig
index 1bd57c7..83b8001 100644
--- a/src/protocol.zig
+++ b/src/protocol.zig
@@ -192,22 +192,22 @@ test "CLIENT_REVISION is sane" {
}
test "ClientPacket enum tags are stable wire bytes" {
- try std.testing.expectEqual(@as(u64, 0), @intFromEnum(ClientPacket.Hello));
- try std.testing.expectEqual(@as(u64, 1), @intFromEnum(ClientPacket.Query));
- try std.testing.expectEqual(@as(u64, 4), @intFromEnum(ClientPacket.Ping));
+ try std.testing.expectEqual(@as(u64, 0), @backingInt(ClientPacket.Hello));
+ try std.testing.expectEqual(@as(u64, 1), @backingInt(ClientPacket.Query));
+ try std.testing.expectEqual(@as(u64, 4), @backingInt(ClientPacket.Ping));
}
test "ServerPacket enum tags are stable wire bytes" {
- try std.testing.expectEqual(@as(u64, 0), @intFromEnum(ServerPacket.Hello));
- try std.testing.expectEqual(@as(u64, 1), @intFromEnum(ServerPacket.Data));
- try std.testing.expectEqual(@as(u64, 2), @intFromEnum(ServerPacket.Exception));
- try std.testing.expectEqual(@as(u64, 5), @intFromEnum(ServerPacket.EndOfStream));
+ try std.testing.expectEqual(@as(u64, 0), @backingInt(ServerPacket.Hello));
+ try std.testing.expectEqual(@as(u64, 1), @backingInt(ServerPacket.Data));
+ try std.testing.expectEqual(@as(u64, 2), @backingInt(ServerPacket.Exception));
+ try std.testing.expectEqual(@as(u64, 5), @backingInt(ServerPacket.EndOfStream));
}
test "CompressionMethodByte values match wire bytes" {
- try std.testing.expectEqual(@as(u8, 0x02), @intFromEnum(CompressionMethodByte.None));
- try std.testing.expectEqual(@as(u8, 0x82), @intFromEnum(CompressionMethodByte.LZ4));
- try std.testing.expectEqual(@as(u8, 0x90), @intFromEnum(CompressionMethodByte.ZSTD));
+ try std.testing.expectEqual(@as(u8, 0x02), @backingInt(CompressionMethodByte.None));
+ try std.testing.expectEqual(@as(u8, 0x82), @backingInt(CompressionMethodByte.LZ4));
+ try std.testing.expectEqual(@as(u8, 0x90), @backingInt(CompressionMethodByte.ZSTD));
}
test "client identification strings are non-empty" {
diff --git a/src/query.zig b/src/query.zig
index 16a49bf..cd4dea1 100644
--- a/src/query.zig
+++ b/src/query.zig
@@ -83,8 +83,8 @@ pub fn writeClientQuery(
try wire.writeStringBinary(writer, "");
}
- try varint.writeVarUInt(writer, @intFromEnum(opts.stage));
- try varint.writeVarUInt(writer, @intFromEnum(opts.compression));
+ try varint.writeVarUInt(writer, @backingInt(opts.stage));
+ try varint.writeVarUInt(writer, @backingInt(opts.compression));
try wire.writeStringBinary(writer, query);
// Query parameters — gated. Empty map encodes to a single sentinel byte.
@@ -139,7 +139,7 @@ test "writeClientQuery emits Query packet ID first" {
var buf: [512]u8 = undefined;
var w: std.Io.Writer = .fixed(&buf);
try writeClientQuery(&w, "SELECT 1", .{}, protocol.CLIENT_REVISION, .{});
- try testing.expectEqual(@as(u8, @intFromEnum(protocol.ClientPacket.Query)), w.buffered()[0]);
+ try testing.expectEqual(@as(u8, @backingInt(protocol.ClientPacket.Query)), w.buffered()[0]);
}
test "writeClientQuery keeps settings and parameters separate" {
@@ -187,7 +187,7 @@ test "writeEmptyDataTerminator at pinned revision emits the BlockInfo prelude" {
try writeEmptyDataTerminator(&w, protocol.CLIENT_REVISION);
const out = w.buffered();
// Data packet id (2), then empty table name (1 byte = 0)
- try testing.expectEqual(@as(u8, @intFromEnum(protocol.ClientPacket.Data)), out[0]);
+ try testing.expectEqual(@as(u8, @backingInt(protocol.ClientPacket.Data)), out[0]);
try testing.expectEqual(@as(u8, 0), out[1]);
// BlockInfo: 1, 0, 2, then i32 LE -1, then 0 (end)
try testing.expectEqual(@as(u8, 1), out[2]);
diff --git a/src/result_stream.zig b/src/result_stream.zig
index 7e1bd7e..33804cc 100644
--- a/src/result_stream.zig
+++ b/src/result_stream.zig
@@ -4,7 +4,7 @@
//! sees `EndOfStream` (or `Exception`, which terminates as well).
//! Callers iterate:
//!
-//! var stream = try client.query("SELECT 1", arena.allocator(), null, .{});
+//! var stream = try client.query("SELECT 1", arena.allocator(), .{});
//! while (try stream.next()) |packet| {
//! switch (packet) {
//! .data => |block| { ... },
@@ -20,6 +20,12 @@
//! when it sees the terminator packet or an Exception. Calling
//! `next()` after that returns `null`.
//!
+//! Cancellation: if the calling task is canceled (`std.Io`) while
+//! `next()` waits for the next packet, the stream asks the Client to
+//! send a protocol `Cancel` and drain the reply, then returns
+//! `error.Canceled` with the Client back at `.ready`. To stop consuming
+//! a result early without canceling the task, call `cancel()`.
+//!
//! This file exceeds 100 lines because the packet-type dispatch is
//! the central response loop and splitting it into per-packet helpers
//! would just add indirection without clarity.
@@ -114,13 +120,24 @@ pub const ResultStream = struct {
is_broken: *bool,
set_ready: *const fn (state: *anyopaque) void,
set_broken: *const fn (state: *anyopaque) void,
+ /// Send `Cancel` and drain to the terminator. Returns true if
+ /// the connection is clean afterwards.
+ abort: *const fn (state: *anyopaque, allocator: std.mem.Allocator, compression: protocol.CompressionEnabled) bool,
};
pub fn next(self: *ResultStream) !?Packet {
if (self.finished) return null;
+ // Packet ids are a single varint byte, so an error here means
+ // nothing of the next packet was consumed: the stream is at a
+ // clean boundary and a canceled query can be aborted in-band.
const packet_id = wire.readServerPacketId(self.reader) catch |e| {
+ const mapped = self.mapReadError(e);
+ if (mapped == error.Canceled) {
+ self.abortOrBreak();
+ return error.Canceled;
+ }
self.markBroken();
- return self.mapReadError(e);
+ return mapped;
};
// Per upstream Connection::receivePacket: Data / Totals / Extremes
// always ride through `maybe_compressed_in` when general
@@ -132,8 +149,7 @@ pub const ResultStream = struct {
// are always uncompressed; encoding the gate now keeps a future
// revision bump from silently hanging on the first Log packet.
const compressed_body = self.compression == .Enable;
- const compressed_log_or_pe = self.compression == .Enable
- and self.server_revision >= protocol.Revision.WITH_COMPRESSED_LOGS_PROFILE_EVENTS_COLUMNS;
+ const compressed_log_or_pe = self.compression == .Enable and self.server_revision >= protocol.Revision.WITH_COMPRESSED_LOGS_PROFILE_EVENTS_COLUMNS;
switch (packet_id) {
.Data => return .{ .data = try self.readBlockAndCheck(compressed_body) },
.Totals => return .{ .totals = try self.readBlockAndCheck(compressed_body) },
@@ -162,6 +178,28 @@ pub const ResultStream = struct {
}
}
+ /// Stop consuming this result early. Sends a protocol `Cancel` to the
+ /// server and drains whatever it still sends, so the parent Client
+ /// returns to `.ready` instead of staying `.busy`. No-op once the
+ /// stream has finished. Leaves the Client broken if the server does
+ /// not wind down within `Config.cancel_drain_timeout_ms`.
+ pub fn cancel(self: *ResultStream) void {
+ if (self.finished) return;
+ self.abortOrBreak();
+ }
+
+ fn abortOrBreak(self: *ResultStream) void {
+ const csv = self.client_state orelse {
+ self.finished = true;
+ return;
+ };
+ if (csv.abort(csv.state, self.allocator, self.compression)) {
+ self.markFinishedReady();
+ } else {
+ self.markBroken();
+ }
+ }
+
fn readBlockAndCheck(self: *ResultStream, compressed_body: bool) !block_mod.Block {
const mode: protocol.CompressionEnabled = if (compressed_body) .Enable else .Disable;
// `table_name` rides UNCOMPRESSED on the wire (mirrors upstream
@@ -183,6 +221,7 @@ pub const ResultStream = struct {
if (self.transport) |t| {
if (t.lastReadError()) |err| {
if (err == error.Timeout) return error.ReadTimeout;
+ if (err == error.Canceled) return error.Canceled;
}
}
}
@@ -261,7 +300,7 @@ test "ResultStream surfaces a Data block then EndOfStream" {
var buf: [256]u8 = undefined;
var w: std.Io.Writer = .fixed(&buf);
// Data packet: ServerPacket.Data = 1, then a 1-col 1-row UInt8 block.
- try varint.writeVarUInt(&w, @intFromEnum(protocol.ServerPacket.Data));
+ try varint.writeVarUInt(&w, @backingInt(protocol.ServerPacket.Data));
try wire.writeStringBinary(&w, "");
try varint.writeVarUInt(&w, 1);
try w.writeByte(0);
@@ -275,7 +314,7 @@ test "ResultStream surfaces a Data block then EndOfStream" {
try w.writeByte(0);
try w.writeByte(7);
// EndOfStream packet: id 5, no body
- try varint.writeVarUInt(&w, @intFromEnum(protocol.ServerPacket.EndOfStream));
+ try varint.writeVarUInt(&w, @backingInt(protocol.ServerPacket.EndOfStream));
var r: std.Io.Reader = .fixed(w.buffered());
var stream: ResultStream = .{
@@ -299,7 +338,7 @@ test "ResultStream surfaces a Data block then EndOfStream" {
test "ResultStream routes Exception terminator" {
var buf: [256]u8 = undefined;
var w: std.Io.Writer = .fixed(&buf);
- try varint.writeVarUInt(&w, @intFromEnum(protocol.ServerPacket.Exception));
+ try varint.writeVarUInt(&w, @backingInt(protocol.ServerPacket.Exception));
try w.writeInt(i32, 60, .little);
try wire.writeStringBinary(&w, "DB::Exception");
try wire.writeStringBinary(&w, "Table not found");
@@ -333,7 +372,7 @@ test "ResultStream with compression on reads table_name BEFORE the frame" {
var w: std.Io.Writer = .fixed(&buf);
// Server packet shape: packet_id Data + table_name "" + frame{ body }
- try varint.writeVarUInt(&w, @intFromEnum(protocol.ServerPacket.Data));
+ try varint.writeVarUInt(&w, @backingInt(protocol.ServerPacket.Data));
try wire.writeStringBinary(&w, ""); // uncompressed table_name
// Build the body that goes inside the frame: BlockInfo + 0 cols + 0 rows.
var body_buf: [32]u8 = undefined;
@@ -372,7 +411,7 @@ test "ResultStream compressed Data round-trips a 1-col 1-row block + asserts exa
var buf: [256]u8 = undefined;
var w: std.Io.Writer = .fixed(&buf);
- try varint.writeVarUInt(&w, @intFromEnum(protocol.ServerPacket.Data));
+ try varint.writeVarUInt(&w, @backingInt(protocol.ServerPacket.Data));
try wire.writeStringBinary(&w, "");
var body_buf: [64]u8 = undefined;
var bw: std.Io.Writer = .fixed(&body_buf);
@@ -418,7 +457,7 @@ test "ResultStream with compression on reads ProfileEvents UNCOMPRESSED below th
var buf: [256]u8 = undefined;
var w: std.Io.Writer = .fixed(&buf);
- try varint.writeVarUInt(&w, @intFromEnum(protocol.ServerPacket.ProfileEvents));
+ try varint.writeVarUInt(&w, @backingInt(protocol.ServerPacket.ProfileEvents));
// Uncompressed block body: table_name "" + BlockInfo + 0 cols + 0 rows
try wire.writeStringBinary(&w, "");
try varint.writeVarUInt(&w, 1);
@@ -455,7 +494,7 @@ test "ResultStream at revision >= 54_481 reads ProfileEvents COMPRESSED via the
var buf: [512]u8 = undefined;
var w: std.Io.Writer = .fixed(&buf);
- try varint.writeVarUInt(&w, @intFromEnum(protocol.ServerPacket.ProfileEvents));
+ try varint.writeVarUInt(&w, @backingInt(protocol.ServerPacket.ProfileEvents));
try wire.writeStringBinary(&w, ""); // uncompressed table_name
// Body: BlockInfo + 0 cols + 0 rows, written into the frame.
var body_buf: [32]u8 = undefined;
@@ -487,7 +526,7 @@ test "ResultStream at revision >= 54_481 reads Log COMPRESSED via the gate" {
var buf: [512]u8 = undefined;
var w: std.Io.Writer = .fixed(&buf);
- try varint.writeVarUInt(&w, @intFromEnum(protocol.ServerPacket.Log));
+ try varint.writeVarUInt(&w, @backingInt(protocol.ServerPacket.Log));
try wire.writeStringBinary(&w, "");
var body_buf: [32]u8 = undefined;
var bw: std.Io.Writer = .fixed(&body_buf);
diff --git a/src/tls_transport.zig b/src/tls_transport.zig
index 680bbe8..544c679 100644
--- a/src/tls_transport.zig
+++ b/src/tls_transport.zig
@@ -86,7 +86,10 @@ pub const TlsTransport = struct {
var ca_bundle_ptr: ?*std.crypto.Certificate.Bundle = null;
var lock_ptr: ?*std.Io.RwLock = null;
- errdefer if (ca_bundle_ptr) |b| { b.deinit(allocator); allocator.destroy(b); };
+ errdefer if (ca_bundle_ptr) |b| {
+ b.deinit(allocator);
+ allocator.destroy(b);
+ };
errdefer if (lock_ptr) |l| allocator.destroy(l);
const tls_opts: std.crypto.tls.Client.Options = blk: {
@@ -159,7 +162,10 @@ pub const TlsTransport = struct {
self.inner.deinit();
self.allocator.destroy(self.inner);
self.allocator.destroy(self.tls_client);
- if (self.ca_bundle) |b| { b.deinit(self.allocator); self.allocator.destroy(b); }
+ if (self.ca_bundle) |b| {
+ b.deinit(self.allocator);
+ self.allocator.destroy(b);
+ }
if (self.ca_lock) |l| self.allocator.destroy(l);
}
diff --git a/src/transport.zig b/src/transport.zig
index da82b12..92d5f48 100644
--- a/src/transport.zig
+++ b/src/transport.zig
@@ -16,15 +16,17 @@
//! `TcpTransport` itself is destroyed by the caller (or by Client when
//! `transport_owned` is set).
//!
-//! Timeout note: Zig 0.16's threaded TCP connect implementation still
+//! Timeout note: Zig 0.17's threaded TCP connect implementation still
//! panics when std's per-connect timeout flag is used. TcpTransport
//! therefore enforces dial timeout by racing the normal connect path
//! against `Io.Timeout.sleep` and canceling the loser. Read/write
-//! timeouts race the socket operation against a timer and retain the
-//! underlying transport error so Client can map it to the public timeout
-//! variants.
+//! timeouts use the native `Io.Batch` operation API (`net_read` /
+//! `net_write` awaited with a timeout), so no helper tasks are spawned
+//! per I/O call. The underlying transport error is retained so Client
+//! can map it to the public timeout variants.
const std = @import("std");
+const builtin = @import("builtin");
const Io = std.Io;
pub const Transport = struct {
@@ -74,6 +76,11 @@ pub const TcpOptions = struct {
write_buffer_size: usize,
/// Connect dial timeout. 0 = infinite (NOT recommended in prod).
dial_timeout_ms: u32 = 30_000,
+ /// Set TCP_NODELAY. The native protocol is request/response with
+ /// small trailing packets (Data terminators, Ping, Cancel); with
+ /// Nagle enabled those wait on the peer's delayed ACK (~40ms on
+ /// Linux) after a large write, which caps INSERT at ~16 per second.
+ no_delay: bool = true,
};
pub const ConnectError = error{
@@ -122,6 +129,7 @@ pub const TcpTransport = struct {
const stream = connectWithTimeout(io, host, port, opts.dial_timeout_ms) catch |e| return e;
errdefer stream.close(io);
+ if (opts.no_delay) setNoDelay(stream);
self.* = .{
.allocator = allocator,
@@ -196,6 +204,24 @@ pub const TcpTransport = struct {
}
};
+/// Best effort: a failure only costs latency, never correctness.
+fn setNoDelay(stream: Io.net.Stream) void {
+ switch (builtin.os.tag) {
+ // std.posix.setsockopt is a compile error on Windows ("use
+ // std.Io instead") and std.Io has no socket-option API yet.
+ .windows, .wasi => return,
+ else => {
+ const one: c_int = 1;
+ std.posix.setsockopt(
+ stream.socket.handle,
+ std.posix.IPPROTO.TCP,
+ std.posix.TCP.NODELAY,
+ std.mem.asBytes(&one),
+ ) catch {};
+ },
+ }
+}
+
const max_iovecs_len = 8;
fn timeoutFromMs(ms: u32) Io.Timeout {
@@ -242,95 +268,28 @@ const TcpReader = struct {
const dest = iovecs_buffer[0..dest_n];
std.debug.assert(dest[0].len > 0);
- const n = if (r.parent.read_timeout_ms) |ms| blk: {
- if (ms == 0) break :blk try r.readNoTimeout(dest);
- break :blk try r.readWithTimeout(dest[0], ms);
- } else try r.readNoTimeout(dest);
-
- if (n == 0) return error.EndOfStream;
- if (n > data_size) {
- r.interface.end += n - data_size;
- return data_size;
- }
- return n;
- }
-
- fn readNoTimeout(r: *TcpReader, dest: [][]u8) Io.Reader.Error!usize {
- const parent = r.parent;
- return parent.io.vtable.netRead(parent.io.userdata, parent.stream.socket.handle, dest) catch |err| {
- r.err = err;
- return error.ReadFailed;
- };
- }
-
- fn readWithTimeout(r: *TcpReader, dest: []u8, ms: u32) Io.Reader.Error!usize {
const parent = r.parent;
- var dests = [_][]u8{dest};
- var queue_storage: [2]ReadResult = undefined;
- var queue: Io.Queue(ReadResult) = .init(&queue_storage);
- var group: Io.Group = .init;
- defer group.cancel(parent.io);
-
- group.concurrent(parent.io, readTask, .{ parent, &dests, &queue }) catch |err| {
+ const result = operateTimeout(parent.io, .{ .net_read = .{
+ .socket_handle = parent.stream.socket.handle,
+ .data = dest,
+ } }, parent.read_timeout_ms) catch |err| {
r.err = err;
return error.ReadFailed;
};
- group.concurrent(parent.io, readTimeoutTask, .{ parent.io, ms, &queue }) catch |err| {
+ const n = (result.net_read catch |err| {
r.err = err;
return error.ReadFailed;
- };
+ }).data_len;
- const first = queue.getOne(parent.io) catch |err| {
- r.err = err;
- return error.ReadFailed;
- };
- switch (first) {
- .read => |n| return n,
- .failed => |err| {
- r.err = err;
- return error.ReadFailed;
- },
- .timed_out => {
- r.err = error.Timeout;
- return error.ReadFailed;
- },
+ if (n == 0) return error.EndOfStream;
+ if (n > data_size) {
+ r.interface.end += n - data_size;
+ return data_size;
}
+ return n;
}
};
-const ReadResult = union(enum) {
- read: usize,
- failed: anyerror,
- timed_out,
-};
-
-fn readTask(parent: *TcpTransport, dest: [][]u8, queue: *Io.Queue(ReadResult)) Io.Cancelable!void {
- const result: ReadResult = if (parent.io.vtable.netRead(parent.io.userdata, parent.stream.socket.handle, dest)) |n|
- .{ .read = n }
- else |err| switch (err) {
- error.Canceled => return error.Canceled,
- else => .{ .failed = err },
- };
- queue.putOne(parent.io, result) catch |err| switch (err) {
- error.Canceled => return error.Canceled,
- error.Closed => {},
- };
-}
-
-fn readTimeoutTask(io: Io, read_timeout_ms: u32, queue: *Io.Queue(ReadResult)) Io.Cancelable!void {
- try Io.Timeout.sleep(timeoutFromMs(read_timeout_ms), io);
- queue.putOne(io, .timed_out) catch |err| switch (err) {
- error.Canceled => return error.Canceled,
- error.Closed => {},
- };
-}
-
-const WriteResult = union(enum) {
- wrote: usize,
- failed: anyerror,
- timed_out,
-};
-
const TcpWriter = struct {
parent: *TcpTransport,
interface: Io.Writer,
@@ -352,52 +311,21 @@ const TcpWriter = struct {
fn drain(io_w: *Io.Writer, data: []const []const u8, splat: usize) Io.Writer.Error!usize {
const w: *TcpWriter = @alignCast(@fieldParentPtr("interface", io_w));
w.err = null;
- const n = if (w.parent.write_timeout_ms) |ms| blk: {
- if (ms == 0) break :blk try w.writeNoTimeout(io_w, data, splat);
- break :blk try w.writeWithTimeout(io_w, data, splat, ms);
- } else try w.writeNoTimeout(io_w, data, splat);
- return io_w.consume(n);
- }
-
- fn writeNoTimeout(w: *TcpWriter, io_w: *Io.Writer, data: []const []const u8, splat: usize) Io.Writer.Error!usize {
- const parent = w.parent;
- return parent.io.vtable.netWrite(parent.io.userdata, parent.stream.socket.handle, io_w.buffered(), data, splat) catch |err| {
- w.err = err;
- return error.WriteFailed;
- };
- }
-
- fn writeWithTimeout(w: *TcpWriter, io_w: *Io.Writer, data: []const []const u8, splat: usize, ms: u32) Io.Writer.Error!usize {
const parent = w.parent;
- var queue_storage: [2]WriteResult = undefined;
- var queue: Io.Queue(WriteResult) = .init(&queue_storage);
- var group: Io.Group = .init;
- defer group.cancel(parent.io);
-
- group.concurrent(parent.io, writeTask, .{ parent, io_w.buffered(), data, splat, &queue }) catch |err| {
- w.err = err;
- return error.WriteFailed;
- };
- group.concurrent(parent.io, writeTimeoutTask, .{ parent.io, ms, &queue }) catch |err| {
+ const result = operateTimeout(parent.io, .{ .net_write = .{
+ .socket_handle = parent.stream.socket.handle,
+ .header = io_w.buffered(),
+ .data = data,
+ .splat = splat,
+ } }, parent.write_timeout_ms) catch |err| {
w.err = err;
return error.WriteFailed;
};
-
- const first = queue.getOne(parent.io) catch |err| {
+ const n = result.net_write catch |err| {
w.err = err;
return error.WriteFailed;
};
- switch (first) {
- .wrote => |n| return n,
- .failed => |err| {
- w.err = err;
- return error.WriteFailed;
- },
- .timed_out => {
- w.err = error.Timeout;
- return error.WriteFailed;
- },
- }
+ return io_w.consume(n);
}
fn sendFile(io_w: *Io.Writer, file_reader: *Io.File.Reader, limit: Io.Limit) Io.Writer.FileError!usize {
@@ -408,31 +336,31 @@ const TcpWriter = struct {
}
};
-fn writeTask(
- parent: *TcpTransport,
- header: []const u8,
- data: []const []const u8,
- splat: usize,
- queue: *Io.Queue(WriteResult),
-) Io.Cancelable!void {
- const result: WriteResult = if (parent.io.vtable.netWrite(parent.io.userdata, parent.stream.socket.handle, header, data, splat)) |n|
- .{ .wrote = n }
- else |err| switch (err) {
- error.Canceled => return error.Canceled,
- else => .{ .failed = err },
- };
- queue.putOne(parent.io, result) catch |err| switch (err) {
- error.Canceled => return error.Canceled,
- error.Closed => {},
- };
-}
-
-fn writeTimeoutTask(io: Io, write_timeout_ms: u32, queue: *Io.Queue(WriteResult)) Io.Cancelable!void {
- try Io.Timeout.sleep(timeoutFromMs(write_timeout_ms), io);
- queue.putOne(io, .timed_out) catch |err| switch (err) {
- error.Canceled => return error.Canceled,
- error.Closed => {},
+/// Run one socket `Operation` under the transport's per-direction
+/// timeout. `null` or `0` means no timeout.
+///
+/// Unlike `Io.operateTimeout`, this always cancels the batch before
+/// returning, so an evented backend never holds a reference into our
+/// stack frame (or into the caller's buffers) after a timeout or task
+/// cancellation. If the operation completed despite the timeout or
+/// cancel request, its result wins and no data is lost; a swallowed
+/// cancel request is re-armed so the next cancellation point fires.
+fn operateTimeout(io: Io, op: Io.Operation, timeout_ms: ?u32) Io.Batch.AwaitConcurrentError!Io.Operation.Result {
+ const ms = timeout_ms orelse 0;
+ if (ms == 0) return io.operate(op);
+
+ var storage: [1]Io.Operation.Storage = undefined;
+ var batch: Io.Batch = .init(&storage);
+ batch.addAt(0, op);
+ batch.awaitConcurrent(io, timeoutFromMs(ms)) catch |err| {
+ batch.cancel(io);
+ if (batch.next()) |completion| {
+ if (err == error.Canceled) io.recancel();
+ return completion.result;
+ }
+ return err;
};
+ return batch.next().?.result;
}
const DialResult = union(enum) {
@@ -493,7 +421,7 @@ fn timeoutTask(io: Io, dial_timeout_ms: u32, queue: *Io.Queue(DialResult)) Io.Ca
fn connectNoStdTimeout(io: Io, host: []const u8, port: u16) ConnectError!Io.net.Stream {
// std.Io.Threaded panics on both Windows and POSIX if
- // options.timeout != .none as of Zig 0.16.0. Always pass .none
+ // options.timeout != .none as of Zig 0.17.1. Always pass .none
// and let connectWithTimeout provide the budget.
if (Io.net.IpAddress.parse(host, port)) |addr| {
return Io.net.IpAddress.connect(&addr, io, .{
@@ -538,7 +466,7 @@ test "Transport vtable round-trips through anyopaque" {
// Synthetic transport over fixed buffers — exercises the vtable
// dispatch without spinning up TCP. (Real TCP is exercised in the
// smoke executable.)
- var read_buf: [16]u8 = .{ 1, 2, 3, 0 } ++ ([_]u8{0} ** 12);
+ var read_buf: [16]u8 = .{ 1, 2, 3, 0 } ++ @as([12]u8, @splat(0));
var write_buf: [16]u8 = undefined;
var rdr: Io.Reader = .fixed(&read_buf);
var wtr: Io.Writer = .fixed(&write_buf);
@@ -561,8 +489,12 @@ test "Transport vtable round-trips through anyopaque" {
}
fn srtImpl(_: *anyopaque, _: ?u32) anyerror!void {}
fn swtImpl(_: *anyopaque, _: ?u32) anyerror!void {}
- fn lreImpl(_: *anyopaque) ?anyerror { return null; }
- fn lweImpl(_: *anyopaque) ?anyerror { return null; }
+ fn lreImpl(_: *anyopaque) ?anyerror {
+ return null;
+ }
+ fn lweImpl(_: *anyopaque) ?anyerror {
+ return null;
+ }
};
var mock: Mock = .{ .r = &rdr, .w = &wtr };
diff --git a/src/wire.zig b/src/wire.zig
index dc7bcce..fe3281b 100644
--- a/src/wire.zig
+++ b/src/wire.zig
@@ -86,7 +86,7 @@ pub fn writeClientPacketId(
writer: *std.Io.Writer,
p: protocol.ClientPacket,
) std.Io.Writer.Error!void {
- try varint.writeVarUInt(writer, @intFromEnum(p));
+ try varint.writeVarUInt(writer, @backingInt(p));
}
/// Read a server-side packet ID as a varint and convert to enum.
diff --git a/tests/audit_probes.zig b/tests/audit_probes.zig
index 335d648..39085f1 100644
--- a/tests/audit_probes.zig
+++ b/tests/audit_probes.zig
@@ -196,7 +196,7 @@ test "audit A9 hostile Sparse count is rejected before sparse index allocation"
test "audit B1 hostile LZ4 decompressed_size is rejected before frame allocation" {
var tail: [9]u8 = undefined;
- tail[0] = @intFromEnum(compression.Method.lz4);
+ tail[0] = @backingInt(compression.Method.lz4);
std.mem.writeInt(u32, tail[1..5], 9, .little);
std.mem.writeInt(u32, tail[5..9], std.math.maxInt(u32), .little);
const sum = cityhash.cityhash128(&tail);
diff --git a/tests/smoke_async.zig b/tests/smoke_async.zig
new file mode 100644
index 0000000..5d83726
--- /dev/null
+++ b/tests/smoke_async.zig
@@ -0,0 +1,289 @@
+//! Async / cancellation smoke scenarios against a real ClickHouse on
+//! 127.0.0.1:9000 (`default:test`), driven through `std.Io` tasks.
+//!
+//! cancel-query — cancel a long-running query from another task;
+//! the client must send Cancel, drain, stay reusable,
+//! and the server must no longer list the query.
+//! stream-cancel — read one block of a huge result, then
+//! `ResultStream.cancel()`; connection stays reusable.
+//! cancel-race — cancel short queries at varying delays; every
+//! outcome must leave the client either reusable (and
+//! actually working) or explicitly broken.
+//! pool-cancel — cancel a task blocked in `Pool.acquire` on a full
+//! pool; no slot may leak.
+//! fanout — run many queries concurrently through a Pool with
+//! `Io.Group`; every result must be correct.
+//! all — run every scenario above.
+//!
+//! Run via: `zig build smoke-async -- `.
+
+const std = @import("std");
+const clickzig = @import("clickzig");
+
+const Io = std.Io;
+
+pub fn main(init: std.process.Init) !void {
+ const gpa = init.gpa;
+ const io = init.io;
+
+ var args = try init.minimal.args.iterateAllocator(gpa);
+ defer args.deinit();
+ _ = args.skip();
+ const scenario = args.next() orelse "all";
+
+ const scenarios = .{
+ .{ "cancel-query", runCancelQuery },
+ .{ "stream-cancel", runStreamCancel },
+ .{ "cancel-race", runCancelRace },
+ .{ "pool-cancel", runPoolCancel },
+ .{ "fanout", runFanout },
+ };
+ var matched = false;
+ inline for (scenarios) |entry| {
+ if (std.mem.eql(u8, scenario, "all") or std.mem.eql(u8, scenario, entry[0])) {
+ matched = true;
+ try entry[1](gpa, io);
+ }
+ }
+ if (!matched) {
+ std.debug.print("unknown scenario: {s}\n", .{scenario});
+ return error.UnknownScenario;
+ }
+}
+
+fn baseConfig(gpa: std.mem.Allocator) clickzig.Config {
+ return .{
+ .control_allocator = gpa,
+ .read_buffer_size = 64 * 1024,
+ .write_buffer_size = 4 * 1024,
+ .username = "default",
+ .password = "test",
+ .dial_timeout_ms = 5_000,
+ };
+}
+
+fn sleepMs(io: Io, ms: i64) !void {
+ const d: Io.Clock.Duration = .{ .raw = .fromMilliseconds(ms), .clock = .awake };
+ try d.sleep(io);
+}
+
+fn nowMs(io: Io) i64 {
+ return Io.Clock.now(.awake, io).toMilliseconds();
+}
+
+/// Run `sql` to completion and return the first UInt64 value seen (or 0).
+fn queryScalar(client: *clickzig.Client, gpa: std.mem.Allocator, sql: []const u8, opts: clickzig.QueryOptions) !u64 {
+ var arena: std.heap.ArenaAllocator = .init(gpa);
+ defer arena.deinit();
+ var stream = try client.query(sql, arena.allocator(), opts);
+ var value: u64 = 0;
+ while (try stream.next()) |packet| switch (packet) {
+ .data => |b| {
+ if (b.num_rows > 0) switch (b.columns[0].column) {
+ .UInt64 => |v| value = v[0],
+ .UInt8 => |v| value = v[0],
+ else => {},
+ };
+ },
+ .exception => |e| {
+ std.debug.print("server exception {d}: {s}\n", .{ e.code, e.message });
+ return error.ServerException;
+ },
+ .end_of_stream => break,
+ else => {},
+ };
+ return value;
+}
+
+fn expectWorking(client: *clickzig.Client, gpa: std.mem.Allocator) !void {
+ if (!client.isReusable()) return error.ClientNotReusable;
+ const one = try queryScalar(client, gpa, "SELECT toUInt64(1)", .{});
+ if (one != 1) return error.FollowUpQueryWrong;
+}
+
+fn serverStillRuns(client: *clickzig.Client, gpa: std.mem.Allocator, io: Io, query_id: []const u8) !bool {
+ var params: clickzig.Parameters = .{};
+ var arena: std.heap.ArenaAllocator = .init(gpa);
+ defer arena.deinit();
+ try params.putString(arena.allocator(), "qid", query_id);
+ // system.processes removal can trail the EndOfStream by a moment.
+ var attempt: u32 = 0;
+ while (attempt < 20) : (attempt += 1) {
+ const n = try queryScalar(client, gpa, "SELECT count() FROM system.processes WHERE query_id = {qid:String}", .{ .parameters = ¶ms });
+ if (n == 0) return false;
+ try sleepMs(io, 50);
+ }
+ return true;
+}
+
+// --- cancel-query ---------------------------------------------------------
+
+fn longQuery(client: *clickzig.Client, gpa: std.mem.Allocator) clickzig.QueryError!u64 {
+ return queryScalar(client, gpa, "SELECT max(number) FROM numbers(100000000000)", .{ .query_id = "smoke-async-cancel" }) catch |e| switch (e) {
+ error.Canceled => error.Canceled,
+ else => error.ProtocolError,
+ };
+}
+
+fn runCancelQuery(gpa: std.mem.Allocator, io: Io) !void {
+ const client = try clickzig.Client.connectTcp(baseConfig(gpa), io, null);
+ defer client.close();
+ const observer = try clickzig.Client.connectTcp(baseConfig(gpa), io, null);
+ defer observer.close();
+
+ var task = try io.concurrent(longQuery, .{ client, gpa });
+ try sleepMs(io, 300);
+ if (!try serverStillRuns(observer, gpa, io, "smoke-async-cancel")) {
+ _ = task.cancel(io) catch {};
+ return error.QueryNotRunningBeforeCancel;
+ }
+
+ const t0 = nowMs(io);
+ const result = task.cancel(io);
+ const elapsed = nowMs(io) - t0;
+ if (result) |_| return error.ExpectedCanceled else |e| if (e != error.Canceled) return e;
+ if (elapsed > 3_000) return error.CancelTooSlow;
+
+ try expectWorking(client, gpa);
+ if (try serverStillRuns(observer, gpa, io, "smoke-async-cancel")) return error.ServerQueryNotKilled;
+ std.debug.print("[cancel-query] OK canceled in {d}ms, client reusable, server query gone\n", .{elapsed});
+}
+
+// --- stream-cancel --------------------------------------------------------
+
+fn runStreamCancel(gpa: std.mem.Allocator, io: Io) !void {
+ const client = try clickzig.Client.connectTcp(baseConfig(gpa), io, null);
+ defer client.close();
+ var arena: std.heap.ArenaAllocator = .init(gpa);
+ defer arena.deinit();
+
+ var settings: clickzig.SettingsMap = .{};
+ try settings.put(arena.allocator(), "max_block_size", "10000");
+ var stream = try client.query("SELECT number FROM numbers(10000000000)", arena.allocator(), .{ .settings = &settings, .query_id = "smoke-async-stream-cancel" });
+ var rows: u64 = 0;
+ while (try stream.next()) |packet| switch (packet) {
+ .data => |b| {
+ rows += b.num_rows;
+ if (rows > 0) break;
+ },
+ .end_of_stream => return error.UnexpectedEnd,
+ else => {},
+ };
+ stream.cancel();
+ if (try stream.next() != null) return error.StreamNotFinished;
+ try expectWorking(client, gpa);
+ std.debug.print("[stream-cancel] OK stopped after {d} rows, client reusable\n", .{rows});
+}
+
+// --- cancel-race ----------------------------------------------------------
+
+fn shortQuery(client: *clickzig.Client, gpa: std.mem.Allocator) clickzig.QueryError!u64 {
+ return queryScalar(client, gpa, "SELECT sum(number) FROM numbers(3000000)", .{}) catch |e| switch (e) {
+ error.Canceled => error.Canceled,
+ error.ClientNotReady => error.ClientNotReady,
+ else => error.ProtocolError,
+ };
+}
+
+fn runCancelRace(gpa: std.mem.Allocator, io: Io) !void {
+ var completed: u32 = 0;
+ var canceled_clean: u32 = 0;
+ var canceled_broken: u32 = 0;
+ var delay: i64 = 0;
+ while (delay < 40) : (delay += 1) {
+ const client = try clickzig.Client.connectTcp(baseConfig(gpa), io, null);
+ defer client.close();
+ var task = try io.concurrent(shortQuery, .{ client, gpa });
+ if (delay > 0) {
+ // 250us steps: spans "canceled mid-query" through "already done".
+ const d: Io.Clock.Duration = .{ .raw = .fromNanoseconds(delay * 250_000), .clock = .awake };
+ try d.sleep(io);
+ }
+ if (task.cancel(io)) |sum| {
+ if (sum != 4499998500000) return error.WrongSum;
+ completed += 1;
+ } else |e| switch (e) {
+ error.Canceled => if (client.isReusable()) {
+ canceled_clean += 1;
+ } else {
+ canceled_broken += 1;
+ continue;
+ },
+ else => return e,
+ }
+ // Any reusable outcome must really be reusable — including the
+ // race where Cancel reaches the server after the query ended.
+ try expectWorking(client, gpa);
+ try expectWorking(client, gpa);
+ }
+ std.debug.print("[cancel-race] OK completed={d} canceled_clean={d} canceled_broken={d}\n", .{ completed, canceled_clean, canceled_broken });
+}
+
+// --- pool-cancel ----------------------------------------------------------
+
+fn runPoolCancel(gpa: std.mem.Allocator, io: Io) !void {
+ const pool = try clickzig.Pool.init(gpa, io, baseConfig(gpa), .{ .max_size = 1 });
+ defer pool.deinit();
+
+ const held = try pool.acquire();
+ var waiter = try io.concurrent(clickzig.Pool.acquire, .{pool});
+ try sleepMs(io, 200);
+ if (waiter.cancel(io)) |c| {
+ pool.release(c);
+ pool.release(held);
+ return error.AcquireShouldHaveBlocked;
+ } else |e| if (e != error.Canceled) return e;
+
+ pool.release(held);
+ const again = try pool.acquire();
+ try again.ping();
+ pool.release(again);
+ if (pool.liveCount() != 1) return error.PoolSlotLeaked;
+ std.debug.print("[pool-cancel] OK blocked acquire canceled, live={d}\n", .{pool.liveCount()});
+}
+
+// --- fanout ---------------------------------------------------------------
+
+const FanoutSlot = struct {
+ n: u64,
+ result: ?u64 = null,
+ err: ?anyerror = null,
+};
+
+fn fanoutTask(pool: *clickzig.Pool, gpa: std.mem.Allocator, slot: *FanoutSlot) Io.Cancelable!void {
+ fanoutInner(pool, gpa, slot) catch |e| switch (e) {
+ error.Canceled => return error.Canceled,
+ else => slot.err = e,
+ };
+}
+
+fn fanoutInner(pool: *clickzig.Pool, gpa: std.mem.Allocator, slot: *FanoutSlot) !void {
+ const client = try pool.acquire();
+ defer pool.release(client);
+ var params: clickzig.Parameters = .{};
+ var arena: std.heap.ArenaAllocator = .init(gpa);
+ defer arena.deinit();
+ try params.putUInt(arena.allocator(), "n", slot.n);
+ slot.result = try queryScalar(client, gpa, "SELECT sum(number) FROM numbers({n:UInt64})", .{ .parameters = ¶ms });
+}
+
+fn runFanout(gpa: std.mem.Allocator, io: Io) !void {
+ const pool = try clickzig.Pool.init(gpa, io, baseConfig(gpa), .{ .max_size = 4 });
+ defer pool.deinit();
+
+ var slots: [24]FanoutSlot = undefined;
+ for (&slots, 0..) |*s, i| s.* = .{ .n = 1_000_000 + i * 1000 };
+
+ var group: Io.Group = .init;
+ defer group.cancel(io);
+ for (&slots) |*s| group.async(io, fanoutTask, .{ pool, gpa, s });
+ try group.await(io);
+
+ for (slots) |s| {
+ if (s.err) |e| return e;
+ const expected = s.n * (s.n - 1) / 2;
+ if (s.result != expected) return error.FanoutWrongResult;
+ }
+ if (pool.liveCount() > 4) return error.PoolOverCap;
+ std.debug.print("[fanout] OK {d} concurrent queries over live={d} connections\n", .{ slots.len, pool.liveCount() });
+}
diff --git a/tests/smoke_connect.zig b/tests/smoke_connect.zig
index 9fb9d5a..8f58ca9 100644
--- a/tests/smoke_connect.zig
+++ b/tests/smoke_connect.zig
@@ -102,7 +102,7 @@ fn drainNoException(stream: *clickzig.ResultStream, label: []const u8) !void {
}
fn runExternalData(allocator: std.mem.Allocator, io: std.Io) !void {
- const client = try clickzig.Client.connectTcp(baseConfig(allocator), io, null, null);
+ const client = try clickzig.Client.connectTcp(baseConfig(allocator), io, null);
defer client.close();
var arena: std.heap.ArenaAllocator = .init(allocator);
defer arena.deinit();
@@ -120,7 +120,6 @@ fn runExternalData(allocator: std.mem.Allocator, io: std.Io) !void {
var stream = try client.query(
"SELECT sum(x) AS total, count() AS n FROM ext_data WHERE label = 'keep'",
arena.allocator(),
- null,
.{ .external_tables = &external },
);
var seen = false;
@@ -132,7 +131,10 @@ fn runExternalData(allocator: std.mem.Allocator, io: std.Io) !void {
if (b.columns[1].column.UInt64[0] != 2) return error.ExternalDataMismatch;
},
.end_of_stream => break,
- .exception => |e| { std.debug.print("[external-data] exception: {s}\n", .{e.message}); return error.QueryFailed; },
+ .exception => |e| {
+ std.debug.print("[external-data] exception: {s}\n", .{e.message});
+ return error.QueryFailed;
+ },
else => {},
};
if (!seen) return error.UnexpectedRowCount;
@@ -142,7 +144,7 @@ fn runExternalData(allocator: std.mem.Allocator, io: std.Io) !void {
fn runCombinedFeatures(allocator: std.mem.Allocator, io: std.Io) !void {
var cfg = baseConfig(allocator);
cfg.compression = .Enable;
- const client = try clickzig.Client.connectTcp(cfg, io, null, null);
+ const client = try clickzig.Client.connectTcp(cfg, io, null);
defer client.close();
var arena: std.heap.ArenaAllocator = .init(allocator);
defer arena.deinit();
@@ -169,7 +171,6 @@ fn runCombinedFeatures(allocator: std.mem.Allocator, io: std.Io) !void {
var stream = try client.query(
"SELECT sum(x) AS total, count() AS n FROM combo_ext WHERE x > {min_x:UInt64} AND label = {wanted:String}",
arena.allocator(),
- null,
.{ .compression = .Enable, .settings = &settings, .parameters = ¶ms, .external_tables = &external },
);
var seen = false;
@@ -181,7 +182,10 @@ fn runCombinedFeatures(allocator: std.mem.Allocator, io: std.Io) !void {
if (b.columns[1].column.UInt64[0] != 2) return error.CombinedFeatureMismatch;
},
.end_of_stream => break,
- .exception => |e| { std.debug.print("[combined-features] exception: {s}\n", .{e.message}); return error.QueryFailed; },
+ .exception => |e| {
+ std.debug.print("[combined-features] exception: {s}\n", .{e.message});
+ return error.QueryFailed;
+ },
else => {},
};
if (!seen) return error.UnexpectedRowCount;
@@ -189,7 +193,7 @@ fn runCombinedFeatures(allocator: std.mem.Allocator, io: std.Io) !void {
}
fn runGeo(allocator: std.mem.Allocator, io: std.Io) !void {
- const client = try clickzig.Client.connectTcp(baseConfig(allocator), io, null, null);
+ const client = try clickzig.Client.connectTcp(baseConfig(allocator), io, null);
defer client.close();
var arena: std.heap.ArenaAllocator = .init(allocator);
defer arena.deinit();
@@ -202,7 +206,7 @@ fn runGeo(allocator: std.mem.Allocator, io: std.Io) !void {
\\ CAST([[(0., 0.), (2., 2.)], [(3., 3.), (4., 4.)]], 'MultiLineString') AS multiline,
\\ CAST([[(0., 0.), (1., 0.), (1., 1.)]], 'Polygon') AS poly,
\\ CAST([[[(0., 0.), (1., 0.), (1., 1.)]]], 'MultiPolygon') AS multipoly
- , arena.allocator(), null, .{});
+ , arena.allocator(), .{});
var seen = false;
while (try stream.next()) |p| switch (p) {
.data => |b| {
@@ -235,13 +239,16 @@ fn runGeo(allocator: std.mem.Allocator, io: std.Io) !void {
if (multipoly.inner.Array.inner.Array.offsets[0] != 3) return error.GeoMismatch;
},
.end_of_stream => break,
- .exception => |e| { std.debug.print("[geo] exception: {s}\n", .{e.message}); return error.QueryFailed; },
+ .exception => |e| {
+ std.debug.print("[geo] exception: {s}\n", .{e.message});
+ return error.QueryFailed;
+ },
else => {},
};
if (!seen) return error.UnexpectedRowCount;
{
- var s = try client.query("DROP TABLE IF EXISTS smoke_geo", arena.allocator(), null, .{});
+ var s = try client.query("DROP TABLE IF EXISTS smoke_geo", arena.allocator(), .{});
try drainNoException(&s, "geo-drop");
}
{
@@ -254,7 +261,7 @@ fn runGeo(allocator: std.mem.Allocator, io: std.Io) !void {
\\ poly Polygon,
\\ multipoly MultiPolygon
\\) ENGINE = Memory
- , arena.allocator(), null, .{});
+ , arena.allocator(), .{});
try drainNoException(&s, "geo-create");
}
@@ -314,9 +321,9 @@ fn runGeo(allocator: std.mem.Allocator, io: std.Io) !void {
.{ .name = "poly", .type_name = "Polygon", .data = .{ .Array = &poly_box } },
.{ .name = "multipoly", .type_name = "MultiPolygon", .data = .{ .Array = &multipoly_box } },
};
- try client.insert("INSERT INTO smoke_geo (p, r, line, multiline, poly, multipoly) FORMAT Native", "", 1, &cols, arena.allocator(), null, .{});
+ try client.insert("INSERT INTO smoke_geo (p, r, line, multiline, poly, multipoly) FORMAT Native", "", 1, &cols, arena.allocator(), .{});
- var back = try client.query("SELECT p, r, line, multiline, poly, multipoly FROM smoke_geo", arena.allocator(), null, .{});
+ var back = try client.query("SELECT p, r, line, multiline, poly, multipoly FROM smoke_geo", arena.allocator(), .{});
var roundtrip_seen = false;
while (try back.next()) |p| switch (p) {
.data => |b| {
@@ -330,7 +337,10 @@ fn runGeo(allocator: std.mem.Allocator, io: std.Io) !void {
if (b.columns[5].column.Array.inner.Array.inner.Array.offsets[0] != 3) return error.GeoMismatch;
},
.end_of_stream => break,
- .exception => |e| { std.debug.print("[geo] roundtrip exception: {s}\n", .{e.message}); return error.QueryFailed; },
+ .exception => |e| {
+ std.debug.print("[geo] roundtrip exception: {s}\n", .{e.message});
+ return error.QueryFailed;
+ },
else => {},
};
if (!roundtrip_seen) return error.UnexpectedRowCount;
@@ -338,7 +348,7 @@ fn runGeo(allocator: std.mem.Allocator, io: std.Io) !void {
}
fn runParameters(allocator: std.mem.Allocator, io: std.Io) !void {
- const client = try clickzig.Client.connectTcp(baseConfig(allocator), io, null, null);
+ const client = try clickzig.Client.connectTcp(baseConfig(allocator), io, null);
defer client.close();
var arena: std.heap.ArenaAllocator = .init(allocator);
defer arena.deinit();
@@ -352,7 +362,6 @@ fn runParameters(allocator: std.mem.Allocator, io: std.Io) !void {
var stream = try client.query(
"SELECT {n:UInt64} + 1 AS answer, {s:String} AS label, toDate({d:String}) AS day",
arena.allocator(),
- null,
.{ .parameters = ¶ms },
);
var seen = false;
@@ -385,12 +394,12 @@ fn customTypeSettings(allocator: std.mem.Allocator) !clickzig.settings.Map {
}
fn runJson(allocator: std.mem.Allocator, io: std.Io) !void {
- const client = try clickzig.Client.connectTcp(baseConfig(allocator), io, null, null);
+ const client = try clickzig.Client.connectTcp(baseConfig(allocator), io, null);
defer client.close();
var arena: std.heap.ArenaAllocator = .init(allocator);
defer arena.deinit();
var settings = try customTypeSettings(arena.allocator());
- var s = try client.query("SELECT toJSONString(CAST('{\"a\":1}' AS JSON)) AS j", arena.allocator(), null, .{ .settings = &settings });
+ var s = try client.query("SELECT toJSONString(CAST('{\"a\":1}' AS JSON)) AS j", arena.allocator(), .{ .settings = &settings });
var seen = false;
while (try s.next()) |p| switch (p) {
.data => |b| {
@@ -409,7 +418,10 @@ fn runJson(allocator: std.mem.Allocator, io: std.Io) !void {
}
},
.end_of_stream => break,
- .exception => |e| { std.debug.print("[json] exception: {s}\n", .{e.message}); return error.QueryFailed; },
+ .exception => |e| {
+ std.debug.print("[json] exception: {s}\n", .{e.message});
+ return error.QueryFailed;
+ },
else => {},
};
if (!seen) return error.UnexpectedRowCount;
@@ -417,12 +429,12 @@ fn runJson(allocator: std.mem.Allocator, io: std.Io) !void {
}
fn runDynamic(allocator: std.mem.Allocator, io: std.Io) !void {
- const client = try clickzig.Client.connectTcp(baseConfig(allocator), io, null, null);
+ const client = try clickzig.Client.connectTcp(baseConfig(allocator), io, null);
defer client.close();
var arena: std.heap.ArenaAllocator = .init(allocator);
defer arena.deinit();
var settings = try customTypeSettings(arena.allocator());
- var s = try client.query("SELECT toString(CAST(number AS Dynamic)) AS d FROM numbers(2)", arena.allocator(), null, .{ .settings = &settings });
+ var s = try client.query("SELECT toString(CAST(number AS Dynamic)) AS d FROM numbers(2)", arena.allocator(), .{ .settings = &settings });
var rows: u64 = 0;
while (try s.next()) |p| switch (p) {
.data => |b| {
@@ -431,7 +443,10 @@ fn runDynamic(allocator: std.mem.Allocator, io: std.Io) !void {
if (b.columns[0].column.len() != b.num_rows) return error.DynamicMismatch;
},
.end_of_stream => break,
- .exception => |e| { std.debug.print("[dynamic] exception: {s}\n", .{e.message}); return error.QueryFailed; },
+ .exception => |e| {
+ std.debug.print("[dynamic] exception: {s}\n", .{e.message});
+ return error.QueryFailed;
+ },
else => {},
};
if (rows != 2) return error.UnexpectedRowCount;
@@ -452,7 +467,7 @@ fn runTimeout(allocator: std.mem.Allocator, io: std.Io) !void {
}
cfg.dial_timeout_ms = 50;
const started = std.Io.Clock.now(.awake, io);
- const result = clickzig.Client.connectTcp(cfg, io, null, null);
+ const result = clickzig.Client.connectTcp(cfg, io, null);
if (result) |c| {
c.close();
return error.UnexpectedSuccess;
@@ -475,7 +490,7 @@ fn runReadTimeout(allocator: std.mem.Allocator, io: std.Io) !void {
cfg.read_timeout_ms = 50;
const started = std.Io.Clock.now(.awake, io);
- const result = clickzig.Client.connectTcp(cfg, io, null, null);
+ const result = clickzig.Client.connectTcp(cfg, io, null);
if (result) |client| {
client.close();
return error.UnexpectedSuccess;
@@ -535,16 +550,19 @@ fn runLowCardinalityWrite(allocator: std.mem.Allocator, io: std.Io) !void {
// INSERT through the client (which now encodes LC frames), SELECT
// back, and verify exact match. Round-trips both the read AND write
// sides of the LC implementation against a real server.
- const client = try clickzig.Client.connectTcp(baseConfig(allocator), io, null, null);
+ const client = try clickzig.Client.connectTcp(baseConfig(allocator), io, null);
defer client.close();
var arena: std.heap.ArenaAllocator = .init(allocator);
defer arena.deinit();
{
- var s = try client.query("DROP TABLE IF EXISTS smoke_lc_w", arena.allocator(), null, .{});
+ var s = try client.query("DROP TABLE IF EXISTS smoke_lc_w", arena.allocator(), .{});
while (try s.next()) |p| switch (p) {
.end_of_stream => break,
- .exception => |e| { std.debug.print("[lc-write] drop failed: {s}\n", .{e.message}); return error.SetupFailed; },
+ .exception => |e| {
+ std.debug.print("[lc-write] drop failed: {s}\n", .{e.message});
+ return error.SetupFailed;
+ },
else => {},
};
}
@@ -560,18 +578,21 @@ fn runLowCardinalityWrite(allocator: std.mem.Allocator, io: std.Io) !void {
\\ label LowCardinality(String),
\\ note LowCardinality(Nullable(String))
\\) ENGINE = Memory
- , arena.allocator(), null, .{});
+ , arena.allocator(), .{});
while (try s.next()) |p| switch (p) {
.end_of_stream => break,
- .exception => |e| { std.debug.print("[lc-write] create failed: {s}\n", .{e.message}); return error.SetupFailed; },
+ .exception => |e| {
+ std.debug.print("[lc-write] create failed: {s}\n", .{e.message});
+ return error.SetupFailed;
+ },
else => {},
};
}
var ks = [_]u32{ 1, 2, 3, 4, 5, 6 };
var labels = [_][]u8{
- @constCast("hot"), @constCast("cold"), @constCast("hot"),
- @constCast("cold"), @constCast("hot"), @constCast("hot"),
+ @constCast("hot"), @constCast("cold"), @constCast("hot"),
+ @constCast("cold"), @constCast("hot"), @constCast("hot"),
};
var note_mask = [_]u8{ 0, 1, 0, 0, 1, 0 };
var note_values = [_][]u8{
@@ -601,12 +622,11 @@ fn runLowCardinalityWrite(allocator: std.mem.Allocator, io: std.Io) !void {
6,
&cols,
arena.allocator(),
- null,
.{ .query_id = "smoke-lc-write" },
);
std.debug.print("[lc-write] inserted 6 rows via LC encoder\n", .{});
- var s = try client.query("SELECT k, label, note FROM smoke_lc_w ORDER BY k", arena.allocator(), null, .{});
+ var s = try client.query("SELECT k, label, note FROM smoke_lc_w ORDER BY k", arena.allocator(), .{});
var seen: u32 = 0;
while (try s.next()) |p| switch (p) {
.data => |b| {
@@ -628,7 +648,10 @@ fn runLowCardinalityWrite(allocator: std.mem.Allocator, io: std.Io) !void {
seen += @intCast(b.num_rows);
},
.end_of_stream => break,
- .exception => |e| { std.debug.print("[lc-write] select failed: {s}\n", .{e.message}); return error.QueryFailed; },
+ .exception => |e| {
+ std.debug.print("[lc-write] select failed: {s}\n", .{e.message});
+ return error.QueryFailed;
+ },
else => {},
};
if (seen != 6) return error.UnexpectedRowCount;
@@ -641,16 +664,19 @@ fn runLowCardinality(allocator: std.mem.Allocator, io: std.Io) !void {
// INSERT...SELECT (we don't ship client-side LC encoding yet),
// then SELECT and verify the materialized data matches the
// pattern we generated server-side.
- const client = try clickzig.Client.connectTcp(baseConfig(allocator), io, null, null);
+ const client = try clickzig.Client.connectTcp(baseConfig(allocator), io, null);
defer client.close();
var arena: std.heap.ArenaAllocator = .init(allocator);
defer arena.deinit();
{
- var s = try client.query("DROP TABLE IF EXISTS smoke_lc", arena.allocator(), null, .{});
+ var s = try client.query("DROP TABLE IF EXISTS smoke_lc", arena.allocator(), .{});
while (try s.next()) |p| switch (p) {
.end_of_stream => break,
- .exception => |e| { std.debug.print("[lc] drop failed: {s}\n", .{e.message}); return error.SetupFailed; },
+ .exception => |e| {
+ std.debug.print("[lc] drop failed: {s}\n", .{e.message});
+ return error.SetupFailed;
+ },
else => {},
};
}
@@ -661,10 +687,13 @@ fn runLowCardinality(allocator: std.mem.Allocator, io: std.Io) !void {
\\ v LowCardinality(String),
\\ m LowCardinality(Nullable(String))
\\) ENGINE = Memory
- , arena.allocator(), null, .{});
+ , arena.allocator(), .{});
while (try s.next()) |p| switch (p) {
.end_of_stream => break,
- .exception => |e| { std.debug.print("[lc] create failed: {s}\n", .{e.message}); return error.SetupFailed; },
+ .exception => |e| {
+ std.debug.print("[lc] create failed: {s}\n", .{e.message});
+ return error.SetupFailed;
+ },
else => {},
};
}
@@ -676,15 +705,18 @@ fn runLowCardinality(allocator: std.mem.Allocator, io: std.Io) !void {
\\ ['alpha', 'beta', 'gamma'][(number % 3) + 1] AS v,
\\ if(number % 4 = 0, NULL, ['x', 'y', 'z'][(number % 3) + 1]) AS m
\\FROM numbers(20)
- , arena.allocator(), null, .{});
+ , arena.allocator(), .{});
while (try s.next()) |p| switch (p) {
.end_of_stream => break,
- .exception => |e| { std.debug.print("[lc] insert-select failed: {s}\n", .{e.message}); return error.SetupFailed; },
+ .exception => |e| {
+ std.debug.print("[lc] insert-select failed: {s}\n", .{e.message});
+ return error.SetupFailed;
+ },
else => {},
};
}
- var s = try client.query("SELECT k, v, m FROM smoke_lc ORDER BY k", arena.allocator(), null, .{});
+ var s = try client.query("SELECT k, v, m FROM smoke_lc ORDER BY k", arena.allocator(), .{});
var seen: u32 = 0;
var nulls: u32 = 0;
while (try s.next()) |p| switch (p) {
@@ -713,7 +745,10 @@ fn runLowCardinality(allocator: std.mem.Allocator, io: std.Io) !void {
seen += @intCast(b.num_rows);
},
.end_of_stream => break,
- .exception => |e| { std.debug.print("[lc] select failed: {s}\n", .{e.message}); return error.QueryFailed; },
+ .exception => |e| {
+ std.debug.print("[lc] select failed: {s}\n", .{e.message});
+ return error.QueryFailed;
+ },
else => {},
};
if (seen != 20) return error.UnexpectedRowCount;
@@ -726,7 +761,7 @@ fn runCompression(allocator: std.mem.Allocator, io: std.Io) !void {
// wrapped in LZ4 compression frames; we decompress and parse.
var cfg = baseConfig(allocator);
cfg.compression = .Enable;
- const client = try clickzig.Client.connectTcp(cfg, io, null, null);
+ const client = try clickzig.Client.connectTcp(cfg, io, null);
defer client.close();
var arena: std.heap.ArenaAllocator = .init(allocator);
defer arena.deinit();
@@ -734,7 +769,6 @@ fn runCompression(allocator: std.mem.Allocator, io: std.Io) !void {
var s = try client.query(
"SELECT number FROM numbers(100)",
arena.allocator(),
- null,
.{ .compression = .Enable },
);
var rows: u64 = 0;
@@ -748,7 +782,10 @@ fn runCompression(allocator: std.mem.Allocator, io: std.Io) !void {
}
},
.end_of_stream => break,
- .exception => |e| { std.debug.print("[compression] exception: {s}\n", .{e.message}); return error.QueryFailed; },
+ .exception => |e| {
+ std.debug.print("[compression] exception: {s}\n", .{e.message});
+ return error.QueryFailed;
+ },
else => {},
};
if (rows != 100) return error.UnexpectedRowCount;
@@ -757,7 +794,7 @@ fn runCompression(allocator: std.mem.Allocator, io: std.Io) !void {
}
fn runTupleMap(allocator: std.mem.Allocator, io: std.Io) !void {
- const client = try clickzig.Client.connectTcp(baseConfig(allocator), io, null, null);
+ const client = try clickzig.Client.connectTcp(baseConfig(allocator), io, null);
defer client.close();
var arena: std.heap.ArenaAllocator = .init(allocator);
defer arena.deinit();
@@ -766,7 +803,7 @@ fn runTupleMap(allocator: std.mem.Allocator, io: std.Io) !void {
\\ (number, toString(number * 10)) AS pair,
\\ map('a', toInt64(1), 'b', toInt64(number)) AS m
\\FROM numbers(2)
- , arena.allocator(), null, .{});
+ , arena.allocator(), .{});
var rows: u64 = 0;
while (try s.next()) |p| switch (p) {
.data => |b| {
@@ -788,7 +825,10 @@ fn runTupleMap(allocator: std.mem.Allocator, io: std.Io) !void {
rows += b.num_rows;
},
.end_of_stream => break,
- .exception => |e| { std.debug.print("[tuple-map] exception: {s}\n", .{e.message}); return error.QueryFailed; },
+ .exception => |e| {
+ std.debug.print("[tuple-map] exception: {s}\n", .{e.message});
+ return error.QueryFailed;
+ },
else => {},
};
if (rows != 2) return error.UnexpectedRowCount;
@@ -796,7 +836,7 @@ fn runTupleMap(allocator: std.mem.Allocator, io: std.Io) !void {
}
fn runDecimalIp(allocator: std.mem.Allocator, io: std.Io) !void {
- const client = try clickzig.Client.connectTcp(baseConfig(allocator), io, null, null);
+ const client = try clickzig.Client.connectTcp(baseConfig(allocator), io, null);
defer client.close();
var arena: std.heap.ArenaAllocator = .init(allocator);
defer arena.deinit();
@@ -806,7 +846,7 @@ fn runDecimalIp(allocator: std.mem.Allocator, io: std.Io) !void {
\\ toIPv4('192.168.1.1') AS v4,
\\ toIPv6('2001:db8::1') AS v6,
\\ toUInt128(toUInt64(0xFFFF) * toUInt64(0x10001)) AS big
- , arena.allocator(), null, .{});
+ , arena.allocator(), .{});
while (try s.next()) |p| switch (p) {
.data => |b| {
if (b.num_rows == 0) continue;
@@ -820,14 +860,17 @@ fn runDecimalIp(allocator: std.mem.Allocator, io: std.Io) !void {
if (price_raw != 12345) return error.DecimalMismatch;
},
.end_of_stream => break,
- .exception => |e| { std.debug.print("[decimal-ip] exception: {s}\n", .{e.message}); return error.QueryFailed; },
+ .exception => |e| {
+ std.debug.print("[decimal-ip] exception: {s}\n", .{e.message});
+ return error.QueryFailed;
+ },
else => {},
};
std.debug.print("[decimal-ip] OK\n", .{});
}
fn runColumnCoverage(allocator: std.mem.Allocator, io: std.Io) !void {
- const client = try clickzig.Client.connectTcp(baseConfig(allocator), io, null, null);
+ const client = try clickzig.Client.connectTcp(baseConfig(allocator), io, null);
defer client.close();
var arena: std.heap.ArenaAllocator = .init(allocator);
defer arena.deinit();
@@ -838,7 +881,7 @@ fn runColumnCoverage(allocator: std.mem.Allocator, io: std.Io) !void {
\\ CAST(42, 'SimpleAggregateFunction(sum, UInt64)') AS saf,
\\ CAST([(1, 'a'), (2, 'b')], 'Nested(id UInt32, name String)') AS nested_col,
\\ toIntervalDay(3) AS interval_day
- , arena.allocator(), null, .{});
+ , arena.allocator(), .{});
var seen = false;
while (try s.next()) |p| switch (p) {
.data => |b| {
@@ -857,7 +900,10 @@ fn runColumnCoverage(allocator: std.mem.Allocator, io: std.Io) !void {
if (interval_day != 3) return error.IntervalMismatch;
},
.end_of_stream => break,
- .exception => |e| { std.debug.print("[column-coverage] exception: {s}\n", .{e.message}); return error.QueryFailed; },
+ .exception => |e| {
+ std.debug.print("[column-coverage] exception: {s}\n", .{e.message});
+ return error.QueryFailed;
+ },
else => {},
};
if (!seen) return error.UnexpectedRowCount;
@@ -876,9 +922,9 @@ fn runDsn(allocator: std.mem.Allocator, io: std.Io) !void {
.write_buffer_size = 4 * 1024,
},
);
- const client = try clickzig.Client.connectTcp(result.config, io, null, null);
+ const client = try clickzig.Client.connectTcp(result.config, io, null);
defer client.close();
- try client.ping(null);
+ try client.ping();
std.debug.print("[dsn] connected via DSN; client_name={s}\n", .{result.config.client_name.?});
std.debug.print("[dsn] OK\n", .{});
}
@@ -890,18 +936,18 @@ fn runPool(allocator: std.mem.Allocator, io: std.Io) !void {
// Acquire 3 distinct clients in parallel-ish (no real threads needed
// for the smoke; just sequential acquire+ping+release with a peek
// at live_count to confirm pool bookkeeping).
- const c1 = try pool.acquire(null);
- try c1.ping(null);
- const c2 = try pool.acquire(null);
- try c2.ping(null);
+ const c1 = try pool.acquire();
+ try c1.ping();
+ const c2 = try pool.acquire();
+ try c2.ping();
std.debug.print("[pool] live={d} idle={d} after 2 acquires\n", .{ pool.liveCount(), pool.idleCount() });
pool.release(c1);
pool.release(c2);
std.debug.print("[pool] live={d} idle={d} after release\n", .{ pool.liveCount(), pool.idleCount() });
// Re-acquire — should hit the idle slot, no new dial.
- const c3 = try pool.acquire(null);
- try c3.ping(null);
+ const c3 = try pool.acquire();
+ try c3.ping();
pool.release(c3);
std.debug.print("[pool] live={d} idle={d} after re-acquire+release (should be 2/2)\n", .{ pool.liveCount(), pool.idleCount() });
if (pool.liveCount() != 2) return error.PoolBookkeepingMismatch;
@@ -915,7 +961,7 @@ fn runPoolLoop(allocator: std.mem.Allocator, io: std.Io) !void {
var total: u64 = 0;
var iter: u64 = 0;
while (iter < 12) : (iter += 1) {
- const client = try pool.acquire(null);
+ const client = try pool.acquire();
var arena: std.heap.ArenaAllocator = .init(allocator);
defer arena.deinit();
@@ -925,7 +971,6 @@ fn runPoolLoop(allocator: std.mem.Allocator, io: std.Io) !void {
var stream = try client.query(
"SELECT {n:UInt64} + 1",
arena.allocator(),
- null,
.{ .parameters = ¶ms },
);
while (try stream.next()) |p| switch (p) {
@@ -949,7 +994,7 @@ fn runPoolLoop(allocator: std.mem.Allocator, io: std.Io) !void {
fn runComplexTypes(allocator: std.mem.Allocator, io: std.Io) !void {
// Hit Array, FixedString, and UUID in one query against the server.
- const client = try clickzig.Client.connectTcp(baseConfig(allocator), io, null, null);
+ const client = try clickzig.Client.connectTcp(baseConfig(allocator), io, null);
defer client.close();
var arena: std.heap.ArenaAllocator = .init(allocator);
defer arena.deinit();
@@ -960,7 +1005,7 @@ fn runComplexTypes(allocator: std.mem.Allocator, io: std.Io) !void {
\\ toFixedString(toString(number), 4) AS fs,
\\ generateUUIDv4() AS id
\\FROM numbers(2)
- , arena.allocator(), null, .{});
+ , arena.allocator(), .{});
var rows: u64 = 0;
while (try s.next()) |p| switch (p) {
.data => |b| {
@@ -984,7 +1029,10 @@ fn runComplexTypes(allocator: std.mem.Allocator, io: std.Io) !void {
rows += b.num_rows;
},
.end_of_stream => break,
- .exception => |e| { std.debug.print("[complex] exception: {s}\n", .{e.message}); return error.QueryFailed; },
+ .exception => |e| {
+ std.debug.print("[complex] exception: {s}\n", .{e.message});
+ return error.QueryFailed;
+ },
else => {},
};
if (rows != 2) return error.UnexpectedRowCount;
@@ -994,7 +1042,7 @@ fn runComplexTypes(allocator: std.mem.Allocator, io: std.Io) !void {
fn runNullableRoundtrip(allocator: std.mem.Allocator, io: std.Io) !void {
// Read a server-generated Nullable column and verify the mask comes
// through. Bypasses INSERT to keep the test scope focused on read.
- const client = try clickzig.Client.connectTcp(baseConfig(allocator), io, null, null);
+ const client = try clickzig.Client.connectTcp(baseConfig(allocator), io, null);
defer client.close();
var arena: std.heap.ArenaAllocator = .init(allocator);
defer arena.deinit();
@@ -1004,7 +1052,7 @@ fn runNullableRoundtrip(allocator: std.mem.Allocator, io: std.Io) !void {
\\ number AS n,
\\ if(number % 2 = 0, NULL, toUInt32(number * 10)) AS maybe_n
\\FROM numbers(5)
- , arena.allocator(), null, .{});
+ , arena.allocator(), .{});
var rows_seen: u64 = 0;
while (try s.next()) |p| switch (p) {
.data => |b| {
@@ -1027,7 +1075,10 @@ fn runNullableRoundtrip(allocator: std.mem.Allocator, io: std.Io) !void {
rows_seen += b.num_rows;
},
.end_of_stream => break,
- .exception => |e| { std.debug.print("[nullable] exception: {s}\n", .{e.message}); return error.QueryFailed; },
+ .exception => |e| {
+ std.debug.print("[nullable] exception: {s}\n", .{e.message});
+ return error.QueryFailed;
+ },
else => {},
};
if (rows_seen != 5) return error.UnexpectedRowCount;
@@ -1046,7 +1097,7 @@ fn runInsertCompression(allocator: std.mem.Allocator, io: std.Io, method: clickz
var cfg = baseConfig(allocator);
cfg.compression = .Enable;
cfg.compression_method = method;
- const client = try clickzig.Client.connectTcp(cfg, io, null, null);
+ const client = try clickzig.Client.connectTcp(cfg, io, null);
defer client.close();
var arena: std.heap.ArenaAllocator = .init(allocator);
defer arena.deinit();
@@ -1062,19 +1113,25 @@ fn runInsertCompression(allocator: std.mem.Allocator, io: std.Io, method: clickz
{
const sql = try std.fmt.allocPrint(arena.allocator(), "DROP TABLE IF EXISTS {s}", .{table_name});
- var s = try client.query(sql, arena.allocator(), null, compressed_opts);
+ var s = try client.query(sql, arena.allocator(), compressed_opts);
while (try s.next()) |p| switch (p) {
.end_of_stream => break,
- .exception => |e| { std.debug.print("[{s}] drop failed: {s}\n", .{ label, e.message }); return error.SetupFailed; },
+ .exception => |e| {
+ std.debug.print("[{s}] drop failed: {s}\n", .{ label, e.message });
+ return error.SetupFailed;
+ },
else => {},
};
}
{
const sql = try std.fmt.allocPrint(arena.allocator(), "CREATE TABLE {s} (id UInt32, label String) ENGINE = Memory", .{table_name});
- var s = try client.query(sql, arena.allocator(), null, compressed_opts);
+ var s = try client.query(sql, arena.allocator(), compressed_opts);
while (try s.next()) |p| switch (p) {
.end_of_stream => break,
- .exception => |e| { std.debug.print("[{s}] create failed: {s}\n", .{ label, e.message }); return error.SetupFailed; },
+ .exception => |e| {
+ std.debug.print("[{s}] create failed: {s}\n", .{ label, e.message });
+ return error.SetupFailed;
+ },
else => {},
};
}
@@ -1098,13 +1155,12 @@ fn runInsertCompression(allocator: std.mem.Allocator, io: std.Io, method: clickz
100,
&cols,
arena.allocator(),
- null,
compressed_opts,
);
std.debug.print("[{s}] inserted 100 rows via {s} frames\n", .{ label, method_label });
const select_sql = try std.fmt.allocPrint(arena.allocator(), "SELECT id, label FROM {s} ORDER BY id", .{table_name});
- var s = try client.query(select_sql, arena.allocator(), null, compressed_opts);
+ var s = try client.query(select_sql, arena.allocator(), compressed_opts);
var rows_seen: u32 = 0;
while (try s.next()) |p| switch (p) {
.data => |b| {
@@ -1119,7 +1175,10 @@ fn runInsertCompression(allocator: std.mem.Allocator, io: std.Io, method: clickz
rows_seen += @intCast(b.num_rows);
},
.end_of_stream => break,
- .exception => |e| { std.debug.print("[{s}] select failed: {s}\n", .{ label, e.message }); return error.QueryFailed; },
+ .exception => |e| {
+ std.debug.print("[{s}] select failed: {s}\n", .{ label, e.message });
+ return error.QueryFailed;
+ },
else => {},
};
if (rows_seen != 100) {
@@ -1133,14 +1192,14 @@ fn runNestedCompressedInsert(allocator: std.mem.Allocator, io: std.Io) !void {
var cfg = baseConfig(allocator);
cfg.compression = .Enable;
cfg.compression_method = .zstd;
- const client = try clickzig.Client.connectTcp(cfg, io, null, null);
+ const client = try clickzig.Client.connectTcp(cfg, io, null);
defer client.close();
var arena: std.heap.ArenaAllocator = .init(allocator);
defer arena.deinit();
const opts: clickzig.query.QueryOptions = .{ .compression = .Enable, .compression_method = .zstd };
{
- var s = try client.query("DROP TABLE IF EXISTS smoke_nested_compressed", arena.allocator(), null, opts);
+ var s = try client.query("DROP TABLE IF EXISTS smoke_nested_compressed", arena.allocator(), opts);
try drainNoException(&s, "nested-compressed-drop");
}
{
@@ -1150,7 +1209,7 @@ fn runNestedCompressedInsert(allocator: std.mem.Allocator, io: std.Io) !void {
\\ vals Array(Nullable(UInt32)),
\\ attrs Map(String, Nullable(UInt32))
\\) ENGINE = Memory
- , arena.allocator(), null, opts);
+ , arena.allocator(), opts);
try drainNoException(&s, "nested-compressed-create");
}
@@ -1193,11 +1252,10 @@ fn runNestedCompressedInsert(allocator: std.mem.Allocator, io: std.Io) !void {
2,
&cols,
arena.allocator(),
- null,
opts,
);
- var s = try client.query("SELECT id, vals, attrs FROM smoke_nested_compressed ORDER BY id", arena.allocator(), null, opts);
+ var s = try client.query("SELECT id, vals, attrs FROM smoke_nested_compressed ORDER BY id", arena.allocator(), opts);
var rows: u64 = 0;
while (try s.next()) |p| switch (p) {
.data => |b| {
@@ -1212,7 +1270,10 @@ fn runNestedCompressedInsert(allocator: std.mem.Allocator, io: std.Io) !void {
if (attrs.values.Nullable.mask[1] != 1 or attrs.values.Nullable.inner.UInt32[2] != 300) return error.NestedCompressedMismatch;
},
.end_of_stream => break,
- .exception => |e| { std.debug.print("[nested-compressed-insert] exception: {s}\n", .{e.message}); return error.QueryFailed; },
+ .exception => |e| {
+ std.debug.print("[nested-compressed-insert] exception: {s}\n", .{e.message});
+ return error.QueryFailed;
+ },
else => {},
};
if (rows != 2) return error.UnexpectedRowCount;
@@ -1223,25 +1284,31 @@ fn runInsertRoundtrip(allocator: std.mem.Allocator, io: std.Io) !void {
// Create a temp Memory-engine table, INSERT 3 rows, SELECT them back,
// verify exact value match. Memory engine drops the table when the
// server restarts so no cleanup needed across runs.
- const client = try clickzig.Client.connectTcp(baseConfig(allocator), io, null, null);
+ const client = try clickzig.Client.connectTcp(baseConfig(allocator), io, null);
defer client.close();
var arena: std.heap.ArenaAllocator = .init(allocator);
defer arena.deinit();
// Drop+create.
{
- var s = try client.query("DROP TABLE IF EXISTS smoke_roundtrip", arena.allocator(), null, .{});
+ var s = try client.query("DROP TABLE IF EXISTS smoke_roundtrip", arena.allocator(), .{});
while (try s.next()) |p| switch (p) {
.end_of_stream => break,
- .exception => |e| { std.debug.print("[insert] drop failed: {s}\n", .{e.message}); return error.SetupFailed; },
+ .exception => |e| {
+ std.debug.print("[insert] drop failed: {s}\n", .{e.message});
+ return error.SetupFailed;
+ },
else => {},
};
}
{
- var s = try client.query("CREATE TABLE smoke_roundtrip (id UInt32, label String) ENGINE = Memory", arena.allocator(), null, .{});
+ var s = try client.query("CREATE TABLE smoke_roundtrip (id UInt32, label String) ENGINE = Memory", arena.allocator(), .{});
while (try s.next()) |p| switch (p) {
.end_of_stream => break,
- .exception => |e| { std.debug.print("[insert] create failed: {s}\n", .{e.message}); return error.SetupFailed; },
+ .exception => |e| {
+ std.debug.print("[insert] create failed: {s}\n", .{e.message});
+ return error.SetupFailed;
+ },
else => {},
};
}
@@ -1259,13 +1326,12 @@ fn runInsertRoundtrip(allocator: std.mem.Allocator, io: std.Io) !void {
3,
&cols,
arena.allocator(),
- null,
.{ .query_id = "smoke-insert" },
);
std.debug.print("[insert] inserted 3 rows\n", .{});
// SELECT them back ordered.
- var s = try client.query("SELECT id, label FROM smoke_roundtrip ORDER BY id", arena.allocator(), null, .{});
+ var s = try client.query("SELECT id, label FROM smoke_roundtrip ORDER BY id", arena.allocator(), .{});
var rows_seen: u32 = 0;
while (try s.next()) |p| switch (p) {
.data => |b| {
@@ -1281,7 +1347,10 @@ fn runInsertRoundtrip(allocator: std.mem.Allocator, io: std.Io) !void {
rows_seen += @intCast(b.num_rows);
},
.end_of_stream => break,
- .exception => |e| { std.debug.print("[insert] select failed: {s}\n", .{e.message}); return error.QueryFailed; },
+ .exception => |e| {
+ std.debug.print("[insert] select failed: {s}\n", .{e.message});
+ return error.QueryFailed;
+ },
else => {},
};
if (rows_seen != 3) {
@@ -1296,7 +1365,7 @@ fn runQueryMixed(allocator: std.mem.Allocator, io: std.Io) !void {
// + DateTime decode in one go. `numbers(N)` gives us a deterministic
// 4-row generator; the SELECT then casts/labels columns to known
// types so the wire response has predictable schema.
- const client = try clickzig.Client.connectTcp(baseConfig(allocator), io, null, null);
+ const client = try clickzig.Client.connectTcp(baseConfig(allocator), io, null);
defer client.close();
var arena: std.heap.ArenaAllocator = .init(allocator);
defer arena.deinit();
@@ -1309,7 +1378,7 @@ fn runQueryMixed(allocator: std.mem.Allocator, io: std.Io) !void {
\\ toDateTime('2026-05-04 09:00:00') + number AS ts
\\FROM numbers(4)
;
- var stream = try client.query(sql, arena.allocator(), null, .{ .query_id = "smoke-query-mixed" });
+ var stream = try client.query(sql, arena.allocator(), .{ .query_id = "smoke-query-mixed" });
var rows_seen: u64 = 0;
while (try stream.next()) |packet| switch (packet) {
.data => |b| {
@@ -1342,13 +1411,13 @@ fn runQueryBytes(allocator: std.mem.Allocator, io: std.Io) !void {
// Full round-trip: send "SELECT 1", drain every server packet through
// EndOfStream, dump what came back. Verifies wire format end-to-end
// including BlockInfo, column header, and primitive column decode.
- const client = try clickzig.Client.connectTcp(baseConfig(allocator), io, null, null);
+ const client = try clickzig.Client.connectTcp(baseConfig(allocator), io, null);
defer client.close();
var arena: std.heap.ArenaAllocator = .init(allocator);
defer arena.deinit();
- var stream = try client.query("SELECT 1", arena.allocator(), null, .{ .query_id = "smoke-query-bytes" });
+ var stream = try client.query("SELECT 1", arena.allocator(), .{ .query_id = "smoke-query-bytes" });
var data_blocks: u32 = 0;
var progress_packets: u32 = 0;
while (try stream.next()) |packet| switch (packet) {
@@ -1387,7 +1456,7 @@ fn baseConfig(allocator: std.mem.Allocator) clickzig.Config {
}
fn runHappy(allocator: std.mem.Allocator, io: std.Io) !void {
- const client = try clickzig.Client.connectTcp(baseConfig(allocator), io, null, null);
+ const client = try clickzig.Client.connectTcp(baseConfig(allocator), io, null);
defer client.close();
const info = client.server_info;
std.debug.print("[happy] connected: {s} {d}.{d}.{d} rev {d}\n", .{
@@ -1399,16 +1468,16 @@ fn runHappy(allocator: std.mem.Allocator, io: std.Io) !void {
});
if (info.timezone) |tz| std.debug.print("[happy] timezone: {s}\n", .{tz});
if (info.display_name) |dn| std.debug.print("[happy] display: {s}\n", .{dn});
- try client.ping(null);
+ try client.ping();
std.debug.print("[happy] ping/pong ok, last_used_at_ms={d}\n", .{client.last_used_at_ms});
}
fn runPing(allocator: std.mem.Allocator, io: std.Io) !void {
- const client = try clickzig.Client.connectTcp(baseConfig(allocator), io, null, null);
+ const client = try clickzig.Client.connectTcp(baseConfig(allocator), io, null);
defer client.close();
const start = client.last_used_at_ms;
var i: usize = 0;
- while (i < 100) : (i += 1) try client.ping(null);
+ while (i < 100) : (i += 1) try client.ping();
std.debug.print("[ping] 100 pings ok; last_used_at_ms drifted from {d} to {d}\n", .{ start, client.last_used_at_ms });
}
@@ -1417,7 +1486,7 @@ fn runWrongPass(allocator: std.mem.Allocator, io: std.Io) !void {
cfg.password = "obviously-wrong";
var diag: clickzig.Diagnostics = .{};
defer diag.deinit();
- const result = clickzig.Client.connectTcp(cfg, io, null, &diag);
+ const result = clickzig.Client.connectTcp(cfg, io, &diag);
if (result) |c| {
c.close();
std.debug.print("[wrong-pass] FAIL: connected with bad password\n", .{});
@@ -1444,7 +1513,7 @@ fn runUnreachable(allocator: std.mem.Allocator, io: std.Io) !void {
var cfg = baseConfig(allocator);
cfg.port = 9001; // Nothing listens here
cfg.dial_timeout_ms = 3_000;
- const result = clickzig.Client.connectTcp(cfg, io, null, null);
+ const result = clickzig.Client.connectTcp(cfg, io, null);
if (result) |c| {
c.close();
std.debug.print("[unreachable] FAIL: connected to dead port\n", .{});
@@ -1458,7 +1527,7 @@ fn runWrongHost(allocator: std.mem.Allocator, io: std.Io) !void {
var cfg = baseConfig(allocator);
cfg.host = "192.0.2.1"; // TEST-NET-1; documented as unroutable
cfg.dial_timeout_ms = 3_000;
- const result = clickzig.Client.connectTcp(cfg, io, null, null);
+ const result = clickzig.Client.connectTcp(cfg, io, null);
if (result) |c| {
c.close();
std.debug.print("[wrong-host] FAIL: somehow connected to TEST-NET-1\n", .{});