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.

CI - Zig 0.16.0 + Zig 0.17 ClickHouse 26.3 tested Apache-2.0

## 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 + +

clickzig benchmark results: read throughput vs clickhouse-client, concurrent query scaling, ping latency, insert throughput

+ +| 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 benchmarksUInt64 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/sclickzig benchmarksZig 0.17.1 · ClickHouse 26.3 · AMD Ryzen 7 7840HS (16 threads) · Linux · Io.ThreadedRead throughput — million rows/s (higher is better)clickhouse-client 26.3 (C++)clickzig0100200300400500UInt64 scan20M rows468455UInt64 scan + LZ420M rows130115String scan5M rows9183String scan + LZ45M rows5762Concurrent queries over a Pool — queries/s48 × SELECT 500k rows (UInt64 + String), Io.Group workers0100200300101115222164265826616workers (connections)Ping round trip (p50)19 µsp99 40 µs · 20k sequential pingsINSERT, 100k rows × 3 columns1.65M rows/sclickhouse-client: 1.64M rows/s · server-boundLoopback 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", .{});