Skip to content

feat(clickhouse): adopt the rebuilt column layer, node formatters, and DDL-source accessors - #5

Merged
vistart merged 13 commits into
release/v1.0.0.dev1from
feature/typed-columns
Oct 11, 2026
Merged

vistart merged 13 commits into
release/v1.0.0.dev1from
feature/typed-columns

Conversation

@vistart

@vistart vistart commented Oct 11, 2026

Copy link
Copy Markdown
Contributor

Description

Aligns this backend with the rebuilt column layer core shipped in release/v1.0.0.dev30 (python-activerecord#126). Core decided that which operations a column offers is a property of its class rather than of a universal base, turned trim / lpad / rpad / repeat into nodes with one format_* hook each, renamed DateTimeColumn to TimestampColumn, and rebuilt the DDLSource accessors so SQL-type reads go through column_data_type() / columns_data_type() instead of column_type() / columns_type(). This branch is this backend's side of that work: eleven commits on top of release/v1.0.0.dev1, with the release tip an ancestor and nothing behind, so no rebase is needed.

The column-type table is the centre of this PR. Core's rebuild makes "which column class does this annotation mean" a question every dialect must answer through the new ColumnTypeSupport protocol (suggested_column_types() / suggested_extra_column_types()), and it removed the neutral common-type table the previous ClickHouseColumnSuggestionMixin derived from. So the backend now states its own answer in full: src/rhosocial/activerecord/backend/impl/clickhouse/mixins/column_type.py holds CLICKHOUSE_COLUMN_TYPES, eighteen entries, every one a column class, none refused. Every cell is backed by a probe against a live ClickHouse 26.7.3.19 server, recorded in .claude/plan/2026-10-08/secondary-gaps-investigation.md (appendix C) and the core plan directory. ClickHouseColumnTypeMixin is composed ahead of core's ColumnTypeMixin in ClickHouseDialect so C3 linearisation lets the backend's override win, ColumnTypeSupport is declared in the protocol-conformance list, and ClickHouseTypeSupport is split into protocols/type.py and placed ahead of DataTypeSupport for the same MRO reason. A dedicated feature/backend/dialect/test_column_type.py asserts the declaration — all eighteen entries present and equal to the measured cells, read through the model layer's own resolve_column_class rather than by asking the mixin directly, because a table that answers correctly but is not reachable by the selection would only fail at query time.

Two cells are deliberately recorded as open questions rather than rulings, and the module docstring says so: the UInt question (议题 C) — ClickHouse has a full unsigned family, but whether "non-negative only" deserves a column class of its own is undecided, so int answers IntegerColumn like every other backend — and Tuple / Map (议题 A), where tuple answers ArrayColumn because that is what the agreed protocol table says, not because Array(T) is the right carrier for a heterogeneous value. The rendering facts found while measuring (LENGTH on a String returning bytes rather than characters, JSON equality needing toString(doc) = ?) are recorded there too, because a future phase will need them, but nothing acts on them yet.

TRIM is the one of the four string nodes ClickHouse spells differently, and it needed new code. ClickHouse does parse the ANSI form, but only when the character set is spelled out: TRIM(BOTH FROM x) with no characters is a syntax error, measured on the public playground (Syntax error: failed at position ... Expected FROM). That is exactly the form the shared default renderer emits for the whitespace case, so simply composing core's TrimMixin was not an option. ClickHouseTrimMixin (mixins/trim.py) is therefore written from scratch: the direction moves into the function name and the operand order becomes target-first, characters-second — BOTH → trimBoth(x[, chars]), LEADING → trimLeft(x[, chars]), TRAILING → trimRight(x[, chars]) — with the operands' parameters carried in order and the optional alias preserved. It is placed ahead of core's TrimMixin in the dialect's base list so it wins. LPAD, RPAD and REPEAT need no override at all: ClickHouse spells the default spellings natively (lpad / rpad as aliases of leftPad / rightPad at 21.8+, repeat at 20.1+), so the branch just composes core's LpadMixin / RpadMixin / RepeatMixin.

The round-trip enumeration test is what makes a missing formatter visible. feature/backend/test_expression_roundtrip_all.py walks both core's expression package and this backend's own — the five backends that already had a matrix of this shape collect only their own, which is how postgres's comment.py:42 read a field core had removed months earlier and stayed green. Every outcome is classified instead of swallowed: a render must restore byte-identical SQL and byte-identical bind parameters through all three encodings; an UnsupportedFeatureError raised by a probe inside a formatter is asserted as exactly that type; and a class the dispatch finds no format_* for must be a row in UNMODELLED_FORMATTERS, a table of 31 method names in 8 groups that names the feature group and the reason for each. That table is pinned in both directions, so a method nobody listed fails the run and a listed method that no class needs fails too. None of the four new string nodes appears in it, because ClickHouse really does spell all four. The reason the blanket branch had to be split is stated in the file's own docstring: a probe's refusal and the dispatch's refusal are the same type, and allowing both identically let a backend shipping no formatter for one of the new nodes read as "not on this engine" and pass.

Tests follow the two core API changes. Reads that used to go through column_type() move to column_data_type(), and columns_type() to columns_data_type(); predicates that the bare Column used to accept are now built explicitly as ComparisonPredicate / InPredicate / BinaryArithmeticExpression nodes, across the DDL-source tests, the derived-field tests and the fixtures. The protocol-conformance list follows DateTimeSupport → TimestampSupport, and UUIDSupport joins the not-implemented list so the omission is a recorded decision rather than a gap.

The branch also carries the catch-up to the current core that these changes require. release/v1.0.0.dev1 predates several core refactors, so the first commit brings this backend level with them:

  • The MySQL 9.0 VECTOR emulation surface is deleted outright (expression/vector.py, mixins/vector.py, protocols/vector.py, ClickHouseVectorMixin, ClickHouseVectorSupport). ClickHouse has no STRING_TO_VECTOR / VECTOR_TO_STRING / VECTOR_DIM / DISTANCE_* API to emulate; its own vector type is QBit, rendered by ClickHouseVectorType → QBit(Float32, n), and Array(Float32) + L2Distance also works. Emitting an API the server does not have is the thing this project's own capability rules forbid.
  • Declarative partitioning goes: the PARTITION ... VALUES strategy family, subpartitioning, and ADD / TRUNCATE / REORGANIZE / EXCHANGE / COALESCE / ANALYZE / CHECK / REBUILD / REPAIR PARTITION are MySQL clauses ClickHouse does not have. What remains is ClickHouse's own maintenance — DROP / DETACH / ATTACH PARTITION ID, addressed by partition id and introspected through system.parts — which is now tested by feature/backend/ddl/test_partition_maintenance.py.
  • ALTER TABLE ... ALTER COLUMN default changes follow ClickHouse's real syntax: SET DEFAULT becomes DEFAULT, and DROP DEFAULT becomes MODIFY COLUMN ... REMOVE DEFAULT. Both are quoted from the server's own rejection messages in mixins/ddl_column.py, which now also refuses UNIQUE (col) and an unnamed FOREIGN KEY ... with the measured reason — the latter is accepted by the server and silently discarded, so emitting it would produce a schema with no foreign key and no enforcement.
  • The SHOW and status introspection is rewritten against the columns the server actually sends. Five statements that used to refuse now work, and the MySQL-versus-ClickHouse column-name differences are documented where they fail silently rather than loudly.
  • Both language docs are corrected against 26.7.3.19 measurements: the text inverted index GA from 26.2 and the deprecation of tokenbf_v1 / ngrambf_v1 for full-text search, the seven real ST_* WKB/MVT codecs and the Geometry / Point column types, the optimizer-hint comment the server parses and discards, the skip-index keyword being TYPE rather than MySQL's USING, and FINAL being accepted only by the engines that collapse duplicates.

Type of change

  • feat: column-type protocol registration, the new node formatters, and the introspection rewrite
  • docs: capability-boundary corrections measured against 26.7.3.19
  • perf
  • test: a real round-trip matrix for this dialect, plus new tests for the column-type table, partition maintenance and introspection
  • chore
  • ci: the core dependency re-pointed at release/v1.0.0.dev30
  • build: pydantic 2.12.2 / pydantic-core 2.41.4 pinned for Python 3.13, because an unpinned resolve in requirements-dev.txt picks a pydantic-core whose only cp313t distribution is an sdist
  • revert

Breaking Change

  • Yes, for end-users (backward-incompatible API changes)
  • Yes, for backend developers (internal architectural changes)

Applicable rhosocial-activerecord Version Range: >=1.0.0.dev30,<2.0.0

Impact on End-Users. The core-side impacts (the bare Column losing its operators, DateTimeColumn becoming TimestampColumn, trim / lpad / rpad / repeat returning nodes instead of function calls, the DDLSource accessors, and a recorded function version floor now refusing rather than producing SQL the server rejects) apply here unchanged. On top of those, this backend's own surface changes:

Change Migration
column_type() no longer answers an SQL type, and columns_type() is gone rename to column_data_type() / columns_data_type()
the MySQL 9.0 VECTOR expressions are removed use ClickHouseVectorType → QBit(Float32, n), or Array(Float32) + L2Distance
the PARTITION ... VALUES strategy family and ADD / TRUNCATE / REORGANIZE / EXCHANGE / COALESCE / ANALYZE / CHECK / REBUILD / REPAIR PARTITION are removed pass PARTITION BY <expr> through storage_options; maintain partitions with DROP / DETACH / ATTACH PARTITION ID; introspect through system.parts
SHOW [FULL] COLUMNS / SHOW INDEX / SHOW PROCESSLIST / SHOW ENGINES / SHOW GRANTS no longer raise code that caught the UnsupportedFeatureError from them no longer sees it; the shapes read back are ClickHouse's, so check the documented column-name differences
ALTER COLUMN default changes render different SQL no action — the previous spelling was rejected by the server outright

Impact on Backend Developers. ClickHouseVectorMixin, ClickHouseVectorSupport, protocols/vector.py and mixins/vector.py are gone, as are the partition strategy mixins and testsuite provider partition.py. A dialect must now compose ClickHouseColumnTypeMixin and ClickHouseTrimMixin, declare ColumnTypeSupport, and keep each ahead of the core class it overrides so C3 gives the backend's answer priority. ClickHouseTypeSupport moved to protocols/type.py and must precede DataTypeSupport in the base list for the same reason.

Related Repositories/Packages

Repository Branch State
python-activerecord release/v1.0.0.dev30 merged (PR #126) — this branch's actions/checkout for core already points at it
python-activerecord-testsuite test/typed-column-contracts not merged yet — the testsuite reference in test.yml is intentionally left on this branch

The testsuite reference is deliberate: that branch has not landed on its release branch (release/v1.0.0.dev17), and pointing at a release that lacks the new typed-column contracts would break every backend at once. It moves when the testsuite merges. The other backends each have their own feature/typed-columns branch and each follows the same order: core, then testsuite, then the backends.

Testing

  • I have added new tests to cover my changes.
  • I have updated existing tests to reflect my changes.
  • Tested against ClickHouse 25.8, 26.3 and 26.7 in CI, across Python 3.10–3.14.
  • All existing tests pass — CI is the gate here, not a local run. Local full-suite runs are forbidden by this project's rules, and the five most recent workflow_dispatch runs of this branch (the last at 2026-10-11) were green across all five matrix pairs.

Test Plan:

CI is the full-suite gate: Python Test and Coverage with ClickHouse runs all five Python / ClickHouse pairs, and each of them installs the core from release/v1.0.0.dev30 and the testsuite from test/typed-column-contracts, so what CI verifies is exactly the target state this PR is meant to produce. The new and changed suites it covers include feature/backend/test_expression_roundtrip_all.py (the enumeration matrix), feature/backend/dialect/test_column_type.py (the eighteen-entry table), feature/backend/ddl/test_partition_maintenance.py, feature/backend/ddl/test_ddl_source_mixin.py (the accessor rename), feature/backend/protocol/test_protocol_conformance.py, and feature/backend/introspection/ (the SHOW output shape and the status introspector).

On Python 3.15: there is deliberately no 3.15 leg here. Core made 3.15 a required CI leg, but clickhouse-connect 1.10.0 declares Requires-Python: <3.15,>=3.10, so this package's requires-python stays >=3.10,<3.15. The pin is inherited from the driver rather than chosen, and it is now recorded in the README so the ceiling is not mistaken for a decision of this repository's. The leg arrives when the driver lifts its own bound. The pydantic 2.12.2 / pydantic-core 2.41.4 pins in requirements-dev.txt mirror the same pins core declares and are restated here because that file is installed before the core package, which would otherwise resolve a pydantic-core whose only cp313t distribution is an sdist; this repository's own matrix has no 3.13t leg today.

Checklist

  • My code follows the project's code style guidelines.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have made corresponding changes to the documentation (docstrings, README.md, docs/en_US, docs/zh_CN).
  • My changes generate no new warnings.
  • Changelog fragment — written as changelog.d/<PR number>.<type>.md once this PR exists, because towncrier numbering binds a fragment to the real PR number.
  • I have verified that my changes do not introduce SQL injection vulnerabilities (rendering stays parameterised; the ALTER COLUMN default path inlines the literal, as ClickHouse requires a literal there, and that is unchanged from before).
  • I have checked for potential performance regressions (the column-type table answers from a fresh dict copy, and the TRIM mapping is one dict lookup on the render path).

Additional Notes

Out of scope, recorded so reviewers do not expect it. The DataType four-table rework is Phase 2b's subject and is not started here; neither is the format_* annotation sweep beyond what this branch touched. The two rendering facts found while measuring (LENGTH returning bytes, JSON equality needing toString) are recorded in mixins/column_type.py's docstring but are deliberately not acted on, because rendering belongs to that later phase.

The broad first commit. 8f72db5 is large (76 files) because release/v1.0.0.dev1 predates several core refactors and this had to catch up with all of them at once. The ten commits after it are the typed-columns work proper. That is recorded here so the size is not a surprise, and because the typed-columns changes could not land without the catch-up: this branch is what has been green against core release/v1.0.0.dev30, and release/v1.0.0.dev1 is not.

vistart added 11 commits October 9, 2026 16:13
…as merged

While these branches were in development, CI installed the core from
`@feature/typed-columns`, because the release branch did not carry the rebuilt
column layer yet. That branch has now merged into `release/v1.0.0.dev30`
(PR #126), so the feature reference is both stale and about to disappear with
branch deletion.

Point the core dependency at `release/v1.0.0.dev30` instead — the same branch
these backends are merging into, which is what the release-side CI already
used. The testsuite reference is deliberately left alone: that branch
(`test/typed-column-contracts`) has not merged into its release yet, and
pointing at a release that lacks the new contracts would break every backend
at once. It moves when the testsuite merges.

ClickHouse expresses the dependency as a `ref:` on `actions/checkout` rather
than as a pip requirement; that one is switched the same way.
@vistart vistart added the enhancement New feature or request label Oct 11, 2026
@vistart vistart self-assigned this Oct 11, 2026
@codecov

codecov Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 74.74048% with 146 lines in your changes missing coverage. Please review.
✅ Project coverage is 70.23%. Comparing base (12c85fd) to head (8b8c472).

Files with missing lines Patch % Lines
...pl/clickhouse/introspection/status_introspector.py 56.75% 73 Missing and 7 partials ⚠️
...cord/backend/impl/clickhouse/show/functionality.py 6.52% 43 Missing ⚠️
...ctiverecord/backend/impl/clickhouse/mixins/trim.py 52.94% 5 Missing and 3 partials ⚠️
...activerecord/backend/impl/clickhouse/cli/status.py 0.00% 3 Missing ⚠️
...ecord/backend/impl/clickhouse/mixins/ddl_column.py 50.00% 3 Missing ⚠️
...impl/clickhouse/introspection/show_introspector.py 91.66% 1 Missing and 1 partial ⚠️
...ctiverecord/backend/impl/clickhouse/mixins/json.py 50.00% 2 Missing ⚠️
...record/backend/impl/clickhouse/mixins/partition.py 93.33% 1 Missing and 1 partial ⚠️
...kend/impl/clickhouse/introspection/introspector.py 0.00% 1 Missing ⚠️
...tiverecord/backend/impl/clickhouse/mixins/types.py 98.79% 0 Missing and 1 partial ⚠️
... and 1 more
Additional details and impacted files
@@                   Coverage Diff                   @@
##           release/v1.0.0.dev1       #5      +/-   ##
=======================================================
+ Coverage                67.75%   70.23%   +2.47%     
=======================================================
  Files                      127      127              
  Lines                     7906     7438     -468     
  Branches                   945      916      -29     
=======================================================
- Hits                      5357     5224     -133     
+ Misses                    2226     1923     -303     
+ Partials                   323      291      -32     
Flag Coverage Δ
clickhouse-26.7 70.23% <74.74%> (+2.47%) ⬆️
python-3.14 70.23% <74.74%> (+2.47%) ⬆️
unittests 70.23% <74.74%> (+2.47%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

This repo had no publish.yml at all, so it was the only backend with no
release path: the other nine all ship the same workflow, and the packages
they publish would be built and uploaded by hand.

The workflow is the one every sibling uses, so the release stays manual and
gated rather than automatic: it only starts from workflow_dispatch (or from
a successful test workflow run), it needs the test job to have passed, and
each publish job runs inside a protected GitHub Environment, which is where
the human approval happens. TestPyPI and PyPI are separate environments with
separate approvals, and trusted publishing replaces any stored credential.

The two environments already exist in this repository with the same
protection rules as postgres, so nothing beyond the workflow file was
needed.
@vistart
vistart merged commit 7c4adc4 into release/v1.0.0.dev1 Oct 11, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant