Validated ClickHouse file staging: developer contract¶
This page explains ownership, dependency boundaries and proof for connector authors, maintainers and operators. Start with the Python staging guide for prerequisites, options and recovery. ADR 0064 records the design boundary.
Execution and authority¶
flowchart LR
S[Original export and genuine receipt] --> W[Wrapper attempt and busy guard]
W --> P[Canonical logical reader and exact RowBinary preparation]
P --> J[Private spool and immutable journal]
J --> R[Controlled client or HTTP adapter]
R --> T[Owned local staging table]
T --> V[COUNT and metadata verification]
V --> C[Original and derived identity rechecks]
C --> H[Durable staged event and existing staging handle]
ClickHouseSink.stage_validated_file delegates to its constructed service. It does not
call stage_payload, schema evolution, source selection or checkpoint services.
ClickHouseValidatedFileService owns the attempt sequence, query ownership and
failure cleanup. The optional validated_file_runner_factory(config, policy) is
constructor DI; production composition selects one controlled adapter. The runtime
query port, finite admission policy, errors, canonical JSON and journal resource port
live in runtime/clickhouse_file_stage_contract.py. SQL identifiers and literals use
explicit escaping.
The shared reader in connectors/bulk_text_codec.py preserves the producer's TAB/LF, UTF-8, control
marker and hex-whitespace rules. Source validation retains its previous error
ordering and unlimited legacy reader mode. Only the new preparer supplies record
and phase budgets. It freezes ordered physical columns and the finite type policy,
converts every cell and validates all rows before CREATE. A held source descriptor
and repeated receipt verification establish source identity; a separately sealed
and rehashed spool establishes derived identity.
Component ownership and dependency injection¶
| Component | Responsibility and dependencies |
|---|---|
ClickHouseSink |
Supplies existing connector/callback bindings to the construction boundary and retains the returned collaborators under its existing attributes |
clickhouse_staging_composition.py |
Constructs the validated-file service, decoder and finalizer in order; owns controlled adapter selection and storage-bound journal wiring |
ClickHouseValidatedFileService and preparer |
Depend on the runtime query/journal ports and finite policy; own sequencing and preparation, respectively |
clickhouse_client_request.py and clickhouse_http_request.py |
Own credentials, options and pure request construction shared by legacy and controlled transports |
bulk_text_codec.py |
Owns the producer codec and its logical reader, including wire grammar and read errors |
clickhouse_file_response.py |
Owns bounded HTTP response framing and raw reads using injected metadata/body limits and remaining-deadline callback; imports only the standard library |
ClickHouseFileAttemptJournal |
Implements private spool storage, immutable event publication and failure inventory |
build_clickhouse_staging_components returns an immutable three-field bundle
(validated_file, decoder, finalizer). Assembly invokes no supplied callback
and does not route execution through the bundle. The default file runner factory
reads the sink's current connector through an explicit provider when invoked;
decoder and finalizer retain the connector supplied at initialization. Table,
create, map-type, count and mutation methods keep their original binding, including
the decoder's map-type signature inspection. Drop/plan/create lambdas keep their
existing delayed lookup. The sink retains build_file_runner as an exact alias
with its historical signature and metadata.
Existing partition clones still forward only connector, state storage, logger and legacy runner classes. They do not acquire new resolver or validated-file factory inheritance. Public clone tests cover both transports, parent/child connection use, staging cleanup after transport failure and source-file retention.
The internal service constructor requires a bound
journal_factory(policy, attempt_id) and no longer accepts storage=. Production
composition uses partial(ClickHouseFileAttemptJournal, storage=StoragePreflightService()).
A custom factory must return ClickHouseFileJournalResource, whose capabilities are
limited to the attempt directory/ID, context management, identity/reserve checks,
partial-file creation and sealing, spool release, event recording and failure
attachment. It cannot select a planner or change preparation policy. The public
sink constructor and stage_validated_file signature remain unchanged.
The historical bulk transport modules retain exact credential/option aliases and
their own execution methods. Public builder overrides still dispatch through the
same methods; request extraction does not reroute execute_query through another
public override. The reader facade in support/bulk_text_file_reader.py and policy
facade in sinks/clickhouse_validated_file_models.py retain exact aliases. Historical
class/function metadata, signatures, dataclass behavior and pickle globals remain
compatible. Internal consumers import the implementation owner directly.
The original ContractValidatedFileArtifact import from contract_artifacts remains
the same class, re-exported from validated_file_artifact. Its new
file_validation_attempt(attempt_id, *, verification_budget=None) context binds a
freshly verified receipt. Busy entry cannot reset an active attempt. A genuine
receipt reissued before entry can be admitted; replacement during the attempt is
rejected, including replacement during verification. complete accepts one exact
non-boolean count matching the original receipt, while active, after all checks.
Exceptional exit resets the latest summary. This grants no terminal source authority.
FileVerificationBudget carries an injected monotonic clock, one absolute deadline
and per-scan byte cap through the existing path/descriptor/authority verifiers.
Scans check initial size and actual bytes before/after fixed reads, including EOF;
pread preserves shared descriptor position. Pinned authority locks have bounded
acquisition and release only after acquisition. Calls without a budget keep legacy
signatures. An authority without an explicit budget parameter is rejected before
invocation; no unbounded fallback is tried. Errors from a supported verifier retain
their identity. File acquisition helpers remain separate from receipt policy.
Query and failure sequence¶
stateDiagram-v2
[*] --> checking
checking --> preparing
preparing --> prepared
prepared --> creating_staging: durable CREATE intent
creating_staging --> loading: UUID and ordered columns verified
loading --> verifying: INSERT acknowledged or empty input
verifying --> staged: COUNT and identities match, spool removed, event durable
checking --> failed_cleaned: admission failure
preparing --> failed_cleaned: bounded preparation failure
creating_staging --> cleanup: error or cancellation
loading --> cleanup: error or cancellation
verifying --> cleanup: error or cancellation
cleanup --> failed_cleaned: terminal query and verified removal
cleanup --> retained_unknown: local or remote state unknown
cleanup --> cleanup_failed: verified cleanup cannot finish
Each mutation has exactly one dpone-b02-<attempt_id>-<create|insert|drop> query ID
and a durable intent before submission. There is no internal retry. Read-only
probes, descriptions and the single COUNT have separate identified requests. The
endpoint binds nonzero server/database UUIDs; stage identity additionally binds
its nonzero table UUID, ownership comment, local MergeTree engine, non-temporary
status and exact ordered physical columns. Metadata is checked after transport too.
Control output pins output_format_json_quote_64bit_integers=0, so COUNT and column
positions are canonical JSON integers. Booleans, strings, missing/multiple rows or
inexact counts fail; estimates never supply count authority.
Client uses bounded nonblocking raw stdin/stdout/stderr and reaps on BaseException; HTTP bounds chunk framing, socket operations, full response and connection closure. A successful mutation needs complete bounded acknowledgment, exact emitted byte count/hash and later count/identity checks. Client exit zero and HTTP status 200 alone are insufficient. HTTP rejects endpoint address drift and does not follow redirects. The caller must exclude load balancing and future endpoint failover.
The HTTP adapter injects its owned response factory through CPython 3.11/3.12's
HTTPConnection.response_class hook, preserving the supplied connection factory.
The standard-library response module receives the two independent byte limits and
the adapter's remaining-deadline callback at construction.
A buffered reader owns the original socket file and checks the same absolute
deadline before and after each raw receive. Closing a Connection: close transport
therefore does not detach response reads from their deadline. One 64 KiB metadata
budget covers status/header lines, chunk sizes, data terminators and trailers;
the response body has its own 64 KiB cap. Fixed-length responses must satisfy their
entire declared length. Chunked responses require unsigned hexadecimal sizes,
exact CRLF data terminators, a zero chunk and the complete trailer terminator.
Response and connection cleanup both run before completion is recorded. A cleanup
failure preserves the original error and retains unresolved local ownership.
The client checks its absolute deadline before and after the final process wait;
an already-exited process cannot authorize a late successful acknowledgment.
On error, stop/join local I/O separately from confirming server termination.
Exact-query KILL ... SYNC with matching finished output or an already observed
terminal acknowledgment can establish remote completion. Empty/missing output is
unknown. A failed CREATE or DROP is subject to the same rule. Unknown execution
retains staging/spool/journal and returns no handle. A live local sender retains
its reader until it can be joined. Known termination permits ownership-checked
cleanup, under the no-concurrent-DDL namespace assumption.
One preparation deadline covers entry verification, preparation/seal and pre-CREATE identity checks. Immediately after INSERT acknowledgment or the empty-input boundary, one verification deadline covers COUNT, metadata, local identity scans, completion and return. Control calls use the lesser remaining phase and selected transport bounds. These are cooperative filesystem limits, not hard OS syscall preemption. The later existing finalizer retains its own authority and timeout policy.
Evidence and operations¶
The journal stores canonical UTF-8 JSON under
<work_directory>/<attempt_id>/events/<six-digit-sequence>/attempt.json. Each event
is published through descriptor-relative immutable-tree creation with no replacement
and fsync. Sequence and previous-event SHA256 advance only after durable confirmation.
A visible file whose writer failed is not a successful event or checkpoint.
source_identity contains the original receipt. derived_identity binds preparation
version, ordered target schema, source receipt hash, binary policy, selected mode,
RowBinary bytes/hash and prepared count. The canonical SHA256 is semantic_id;
random attempt IDs, file paths, credentials and timestamps are excluded. Distinct
attempts for identical source/representation inputs have the same semantic identity
and independent resources. This is not global deduplication.
Events record phase/outcome, query identity, endpoint, planned/observed ownership,
counts, local/remote observations, error code and cleanup inventory. Counts unknown
at that point remain null. Empty input records query_kind=insert, null query ID
and not_submitted_empty states while the separate COUNT observes zero. Journals
exclude source values, source path and credentials. The spool contains data and
must be protected by the caller's retention/access policy.
Final evidence follows spool cleanup and source completion verification. The exact
record and journal path are attached to handle.metadata['validated_file_consumption'].
On failure, the last confirmed record is attached to the primary exception's
details. Failed failure-record publication adds a note and preserves the last
confirmed inventory. See the operator recovery procedure.
Validation and customer journey¶
The local test producer uses genuine source receipts, public sink entrypoints, real child-process/loopback HTTP adapters and an independent RowBinary parser. It checks standalone NULL-like strings, controls, literal markers, quotes, Unicode, duplicate rows, all 256 byte values and LEB128 boundaries. A separate comparison checks the real producer codec against independently authored source bytes. Corrupted/reordered/trailing data cannot pass the oracle. Fault tests cover source authority, phase deadlines, journal ordering, count rejection, exact-query cancellation and retained unknown CREATE/INSERT/DROP state. Compatibility characterization also checks historical pickle payloads, signatures, resolved type hints, dataclass defaults and public subclass dispatch through real child processes and loopback HTTP. These checks distinguish preserved transport behavior from the controlled staging adapter's stricter admission policy.
For installed-runtime verification, set DPONE_TEST_B02_EVIDENCE_ROOT to an external
output root and DPONE_TEST_B02_CANDIDATE to the exact candidate SHA. The actual
reconciliation tests emit <root>/<client|http>/value-reconciliation.json only after
comparisons succeed. They include expected/observed values or byte hashes and
non_live=true. Their explicit scope is text/binary value reconciliation; scalar,
empty-input and negative-case proof stays in the complete pytest/JUnit results.
Case keys include row ordinals so identical duplicate values remain distinct.
Without a supplied candidate, identity is UNVERIFIED/WIP.
Use a fresh root such as /var/tmp/b02-evidence/<candidate-sha>/<run-id> for each
run. These UTF-8 test reports overwrite the same per-mode filename on rerun; an
interrupted write may be partial. Consume them only with a successful complete
test run and preserved raw hashes. They are separate from the immutable runtime
journal and do not provide its durability guarantees.
Run every collected B02 test, retaining raw logs, node inventory and both producer
outputs; do not infer coverage from a filtered passing subset.
uv run pytest tests/test_b02_*.py tests/test_file_verification_budget.py \
tests/test_file_contract_validation.py tests/test_mssql_artifact_integrity.py -q
uv run python tools/agent_policy/select_checks.py --base-ref origin/master
The repository's Python, architecture, documentation, full non-live and installed checks plus an independent review of the exact commit remain completion gates. Live value readback requires a separately approved environment and credentials; local protocol peers are not ClickHouse certification. Performance is unmeasured.
| Journey | Entry or observable result |
|---|---|
| Discover | Staging guide distinguishes this explicit API from the ordinary route |
| Prepare/configure | Genuine receipt, finite representation table, named work directory and same-node settings |
| Execute/observe | One method; exact count and durable staged metadata |
| Diagnose/recover | Safe error location, original exception, inventory and manual unknown-state procedure |
| Operate | Capacity reserve, bounded I/O, caller-managed journals and private namespace |
| Upgrade | Additive API; reconcile retained attempts before caller/package rollback |
No CLI command, manifest schema, Airflow/dbt integration, source-finalization or release-publication authority is added. See architecture, compatibility and fast-path contracts.