feat(typelayer)!: validate, coerce and filter through chtypes - #589
EricAndrechek wants to merge 13 commits into
Conversation
WaveHouse stops modelling ClickHouse itself. Ingest validation, coercion, DEFAULT substitution, timestamp parsing, insert checks and row-level security run through chtypes (github.com/wave-rf/chtypes/go v0.2.1, ABI rev 4): the request body goes to one RowsExport as-is with the Content-Type as the declared format (JSON family, CSV, TSV), column policy and auto-inject are per-role compiled schemas, checks and row filters are chtypes filters with every claim bound as a String parameter, and the published row is the bytes ClickHouse's own writer produced. /v1/query and pipes pass ClickHouse's JSONEachRow through. BREAKING: cgo is unconditional; platforms narrow to linux/amd64, linux/arm64, darwin/arm64; Linux binaries need glibc >= 2.34; per-record errors carry ClickHouse's code (a denied column is 117, not 403); wire and query timestamps use ClickHouse's rendering; Decimal is a JSON number. Deleted: discovery validation/timestamp, compact encoder, policy row filter/numeric comparator, record readers, ingest seams, clickhouse_exec, every CGO_ENABLED=0 path and the audit-cgo target. Releases build each target natively on free GitHub runners. make ci green on this tree (unit 1676 -race, integration 89, e2e 84, Go total coverage 92.2%). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Y3XxjTbLMPaNw7kg1KMDi
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018uEnYtmudjD1nn3T44zuhB
- ReconstructDDL is a Library method; identifiers are quoted by the library's own QuoteIdentifier, once per compile. - Insert check clauses are answered by the same parse as the verdicts (RowsExportWith + WithRowFilter); CheckVerdicts and the accepted-row cursor are gone. The parse outcome still decides first. - Library.QuoteLiteral replaces the hand-written DEFAULT literal escaper and the defensive column re-read. - The registry is lazy: artifact errors surface at the first Bind. - CSVWithNames/TSVWithNames via `header=present`; positional CSV/TSV keep header detection off, preserving the documented contract. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018uEnYtmudjD1nn3T44zuhB
Integration and e2e cases for text/csv and text/tab-separated-values with header=present (reordered header, omitted column takes its DEFAULT, unknown header column is a whole-request 400 with code 117), and the ingest section of api.md for the new content types, header detection being off for the positional pair, and checks answered in one parse. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018uEnYtmudjD1nn3T44zuhB
…docs for one-parse checks and header=present Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018uEnYtmudjD1nn3T44zuhB
…tables keep one Linux arm64 measurements show own handles scale 4-6x at N=8 while a serialization gate never beats one thread. Role shapes (LRU 256) keep a single handle to bound memory. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018uEnYtmudjD1nn3T44zuhB
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018uEnYtmudjD1nn3T44zuhB
header=present is CSVWithNames, header=absent is strictly positional (detect_header=0), and a bare type keeps ClickHouse's default header auto-detection. typelayer gains IngestWith/IngestOptions.StrictPositional. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018uEnYtmudjD1nn3T44zuhB
…olumn Measured against ClickHouse 26.6.3.62 and 25.8.33.6 (and chtypes, cell for cell): a String-bound value >= 2^64 wraps modulo 2^64 before comparison on UInt8 and UInt32 columns too, not only 64-bit ones; 128/256-bit columns also wrap at their own width. The earlier caveat said narrow columns were safe. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018uEnYtmudjD1nn3T44zuhB
…arning about wrap Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018uEnYtmudjD1nn3T44zuhB
A policy claim bound as a plain {p:String} wraps modulo 2^64 on every
integer column (and at their own width on [U]Int128/[U]Int256), on
/v1/query, the stream row filter and the insert check alike, so a claim
of 18446744073709551621 read and wrote tenant 5.
Every claim compared against an integer column (Nullable/LowCardinality
included) now binds through
if(toString(accurateCastOrNull({p:String}, 'T')) = {p:String},
accurateCastOrNull({p:String}, 'T'), NULL)
with T the bare integer type. In-range canonical claims answer exactly
as before and keep the primary key in use; out-of-range and
non-canonical claims match nothing on every operator, and an insert
check refuses them with 403 (a non-canonical claim used to be code 53:
no rows on read, 422 on insert). Other column types and caller filters
keep the plain form.
- chsql: IntegerType (picks the form from the column's type string) and
StrictInt (renders it), shared by both paths; IntParam carries a
claim the builder's NamedParams expands to StrictInt over one param.
- policy: WhereClause/WhereParams (rendered at Evaluate, with no column
types) become ResolvedSelect.WhereSQL(colType), called by the builder
with the discovered schema.
- typelayer: render() writes StrictInt for integer columns, for the
stream filter and the ingest check; _in casts each element.
- tests: the stream/query differential now drives the production
/v1/query handler and checks integer shapes (UInt8 through Int256,
Nullable(UInt64)) against the exact answer; new ingest integration
test; unit tables for the type function, the rendered SQL and the
boundary claims on chtypes.
- docs: access-control caution, api.md, architecture.md, CHANGELOG.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018uEnYtmudjD1nn3T44zuhB
|
Superseded by #712, which redoes this migration on current |
Summary
WaveHouse stops modelling ClickHouse itself. Validation, coercion, DEFAULT substitution, timestamp parsing, row-level security and insert checks run through chtypes (
github.com/wave-rf/chtypes/gov0.4.0, ABI revision 6): a cgodlopenof a per-ClickHouse-version shared library that runs the server's own parser and analyzer in-process. The hand-written Go that used to approximate ClickHouse is deleted.Breaking, by design: cgo is unconditional (no pure-Go build), supported platforms narrow to linux/amd64, linux/arm64 and darwin/arm64, the Linux binaries are dynamically linked (glibc ≥ 2.34), and several documented behaviours move to "whatever ClickHouse does" (listed below).
What changed
Ingest — the request body goes to chtypes as-is.
Content-Typedeclares the format: the JSON family (object, array, NDJSON are one format),text/csvandtext/tab-separated-values. The RFC 4180headerparameter maps three ways:header=presentreadsCSVWithNames/TSVWithNames,header=absentreads positionally with ClickHouse's header detection off, and no parameter keeps ClickHouse's own default (it detects a header line); any other value is415. One parse per body:RowsExportWithwith a row filter returns each record's parse outcome and itscheckverdict together, so there is no Go-side decode, re-encode or second parse. Column policy and_eqauto-inject are per-role compiled schemas;checkclauses and row filters are chtypes filters over the parsed rows with every claim bound as aStringparameter. Per-record rejections carry ClickHouse'scode. The published row is the exact bytes ClickHouse's own writer produced, so SSE, the table and/v1/queryagree by construction (#372).Integer claims — a claim bound as a plain
{p:String}and compared against an integer column wraps modulo 2^64 (and at their own width on[U]Int128/[U]Int256), on the server and in chtypes alike, so a claim of18446744073709551621read and wrote tenant5. Row filters, insert checks and the/v1/querypolicy predicate now compare integer columns (Nullable/LowCardinalityincluded) againstif(toString(accurateCastOrNull({p:String}, 'T')) = {p:String}, accurateCastOrNull({p:String}, 'T'), NULL): an in-range canonical claim answers exactly as before and keeps the primary key in use; an out-of-range or non-canonical claim (007,+5,1.0) matches no row and fails an insert check with403. Ingested data is unaffected ("007"still stores7).Query path —
/v1/queryand pipes ask ClickHouse forFORMAT JSONEachRowover HTTP and pass the bytes through; the 424-line result transformer is gone.Identifiers and literals —
chsql.QuoteIdentis byte-identical to ClickHouse'sbackQuote(a test compares it with the SDK'sQuoteIdentifierover every byte); DDL literals use the SDK'sQuoteLiteral.Build/CI/release —
CGO_ENABLED=1everywhere,go 1.27, glibc builder +distroless/ccruntime that bakes the pinned artifact (chtypes.lock,scripts/fetch-chtypes.sh --frozen; the lock pins the revision-6 26.6.8.7 build), golangci-lint v2.13.2,audit-cgoremoved. Releases build each target natively onubuntu-latest,ubuntu-24.04-armandmacos-latest(cgo darwin cannot be cross-built from Linux: the Prometheus client's darwin collector needs a Mach header); one job assembles archives, checksums, provenance, the GHCR image and the Release.Deleted —
internal/discovery/{validation,timestamp}.go,internal/ingest/compact.go,internal/policy/{rowfilter,numeric}.go,internal/api/{record_reader,ingest_seams,clickhouse_exec}.go,policy.LiteralValue/CanonicalNumericLiteral, the two differential-oracle integration tests, everyCGO_ENABLED=0path.Size, measured — production Go is 17,114 lines at the base commit and 18,007 here (non-test
.gooutsidetests/): the migration adds about 890 lines (695 at this PR's first push). An earlier version of this description said "about 800 lines fewer thanmain"; that counted only some files and was wrong. The migration trades hand-written ClickHouse modelling for a wrapper around the real thing; it does not shrink the codebase.Documented contract changes (please review these first)
403 column "x" not allowed for insert400 {"code":117,"error":"Unknown field …"}(does not reveal whether the column exists)_incheck on an absent column007,+5,1.0, ≥ 2^64) on an integer column403on an insert checknullon a checked columntext/csv/text/tab-separated-valuesingestheader=present/header=absent/ no parameter (ClickHouse detects a header)Z2026-06-21 04:00:00.123/v1/queryDecimal/v1/querykey order/v1/querynull filter value/v1/queryinlistArray(String)param, ~6,000-element ceilingdeclinefilter(the strict cast makes it a non-match)Verification
make cigreen on this exact tree: Go unit 1,774 (-race), integration 95, e2e 87 (1 skipped), TS unit 229, Go total coverage 92.6%, no threshold lowered.SELECT … FORMAT JSONCompactEachRowof the stored row; stream row-filter verdicts equal the production/v1/queryhandler's answer on every column shape × operator × claim, including hostile spellings, escaped values and integer claims at and beyond every width (8,838 cells, zero disagreements); a/v1/querytype-rendering pin test.tmp/review-skips-<sha>.log); they run on the redo PR.First-run risks on GitHub
e2eon a cold cache is the long pole (timeout raised to 35 min); if CI is red, read that job's log first and check for a timeout rather than a failure.ubuntu-24.04-armandmacos-latest(advisory validation job).artifacts.wavehouse.devis a CI dependency on a cache miss.Known limits (carried into the redo)
chtypes.Timezoneis process-global and fixed when a ClickHouse version's library is first opened; a per-callsession_timezoneis accepted and ignored. One process serves one server time zone per ClickHouse version.LowCardinality(<integer>)column are declined by chtypes (fail closed).min(GOMAXPROCS, 8); re-measure on deployment hardware.🤖 Generated with Claude Code
https://claude.ai/code/session_018uEnYtmudjD1nn3T44zuhB