Repository navigation
feat(clickhouse): adopt the rebuilt column layer, node formatters, and DDL-source accessors - #5
Merged
Merged
Conversation
… column_data_type)
…tively, trim via trimBoth/Left/Right)
…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.
Codecov Report❌ Patch coverage is 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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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, turnedtrim/lpad/rpad/repeatinto nodes with oneformat_*hook each, renamedDateTimeColumntoTimestampColumn, and rebuilt theDDLSourceaccessors so SQL-type reads go throughcolumn_data_type()/columns_data_type()instead ofcolumn_type()/columns_type(). This branch is this backend's side of that work: eleven commits on top ofrelease/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
ColumnTypeSupportprotocol (suggested_column_types()/suggested_extra_column_types()), and it removed the neutral common-type table the previousClickHouseColumnSuggestionMixinderived from. So the backend now states its own answer in full:src/rhosocial/activerecord/backend/impl/clickhouse/mixins/column_type.pyholdsCLICKHOUSE_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.ClickHouseColumnTypeMixinis composed ahead of core'sColumnTypeMixininClickHouseDialectso C3 linearisation lets the backend's override win,ColumnTypeSupportis declared in the protocol-conformance list, andClickHouseTypeSupportis split intoprotocols/type.pyand placed ahead ofDataTypeSupportfor the same MRO reason. A dedicatedfeature/backend/dialect/test_column_type.pyasserts the declaration — all eighteen entries present and equal to the measured cells, read through the model layer's ownresolve_column_classrather 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
UIntquestion (议题 C) — ClickHouse has a full unsigned family, but whether "non-negative only" deserves a column class of its own is undecided, sointanswersIntegerColumnlike every other backend — andTuple/Map(议题 A), wheretupleanswersArrayColumnbecause that is what the agreed protocol table says, not becauseArray(T)is the right carrier for a heterogeneous value. The rendering facts found while measuring (LENGTHon aStringreturning bytes rather than characters, JSON equality needingtoString(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'sTrimMixinwas 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'sTrimMixinin the dialect's base list so it wins.LPAD,RPADandREPEATneed no override at all: ClickHouse spells the default spellings natively (lpad/rpadas aliases ofleftPad/rightPadat 21.8+,repeatat 20.1+), so the branch just composes core'sLpadMixin/RpadMixin/RepeatMixin.The round-trip enumeration test is what makes a missing formatter visible.
feature/backend/test_expression_roundtrip_all.pywalks 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'scomment.py:42read 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; anUnsupportedFeatureErrorraised by a probe inside a formatter is asserted as exactly that type; and a class the dispatch finds noformat_*for must be a row inUNMODELLED_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 tocolumn_data_type(), andcolumns_type()tocolumns_data_type(); predicates that the bareColumnused to accept are now built explicitly asComparisonPredicate/InPredicate/BinaryArithmeticExpressionnodes, across the DDL-source tests, the derived-field tests and the fixtures. The protocol-conformance list followsDateTimeSupport→TimestampSupport, andUUIDSupportjoins 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.dev1predates several core refactors, so the first commit brings this backend level with them:VECTORemulation surface is deleted outright (expression/vector.py,mixins/vector.py,protocols/vector.py,ClickHouseVectorMixin,ClickHouseVectorSupport). ClickHouse has noSTRING_TO_VECTOR/VECTOR_TO_STRING/VECTOR_DIM/DISTANCE_*API to emulate; its own vector type isQBit, rendered byClickHouseVectorType→QBit(Float32, n), andArray(Float32)+L2Distancealso works. Emitting an API the server does not have is the thing this project's own capability rules forbid.PARTITION ... VALUESstrategy family, subpartitioning, andADD/TRUNCATE/REORGANIZE/EXCHANGE/COALESCE/ANALYZE/CHECK/REBUILD/REPAIR PARTITIONare MySQL clauses ClickHouse does not have. What remains is ClickHouse's own maintenance —DROP/DETACH/ATTACH PARTITION ID, addressed by partition id and introspected throughsystem.parts— which is now tested byfeature/backend/ddl/test_partition_maintenance.py.ALTER TABLE ... ALTER COLUMNdefault changes follow ClickHouse's real syntax:SET DEFAULTbecomesDEFAULT, andDROP DEFAULTbecomesMODIFY COLUMN ... REMOVE DEFAULT. Both are quoted from the server's own rejection messages inmixins/ddl_column.py, which now also refusesUNIQUE (col)and an unnamedFOREIGN 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.SHOWand 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.textinverted index GA from 26.2 and the deprecation oftokenbf_v1/ngrambf_v1for full-text search, the seven realST_*WKB/MVT codecs and theGeometry/Pointcolumn types, the optimizer-hint comment the server parses and discards, the skip-index keyword beingTYPErather than MySQL'sUSING, andFINALbeing accepted only by the engines that collapse duplicates.Type of change
feat: column-type protocol registration, the new node formatters, and the introspection rewritedocs: capability-boundary corrections measured against 26.7.3.19perftest: a real round-trip matrix for this dialect, plus new tests for the column-type table, partition maintenance and introspectionchoreci: the core dependency re-pointed atrelease/v1.0.0.dev30build: pydantic 2.12.2 / pydantic-core 2.41.4 pinned for Python 3.13, because an unpinned resolve inrequirements-dev.txtpicks a pydantic-core whose only cp313t distribution is an sdistrevertBreaking Change
Applicable
rhosocial-activerecordVersion Range:>=1.0.0.dev30,<2.0.0Impact on End-Users. The core-side impacts (the bare
Columnlosing its operators,DateTimeColumnbecomingTimestampColumn,trim/lpad/rpad/repeatreturning nodes instead of function calls, theDDLSourceaccessors, 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:column_type()no longer answers an SQL type, andcolumns_type()is gonecolumn_data_type()/columns_data_type()VECTORexpressions are removedClickHouseVectorType→QBit(Float32, n), orArray(Float32)+L2DistancePARTITION ... VALUESstrategy family andADD/TRUNCATE/REORGANIZE/EXCHANGE/COALESCE/ANALYZE/CHECK/REBUILD/REPAIR PARTITIONare removedPARTITION BY <expr>throughstorage_options; maintain partitions withDROP/DETACH/ATTACH PARTITION ID; introspect throughsystem.partsSHOW [FULL] COLUMNS/SHOW INDEX/SHOW PROCESSLIST/SHOW ENGINES/SHOW GRANTSno longer raiseUnsupportedFeatureErrorfrom them no longer sees it; the shapes read back are ClickHouse's, so check the documented column-name differencesALTER COLUMNdefault changes render different SQLImpact on Backend Developers.
ClickHouseVectorMixin,ClickHouseVectorSupport,protocols/vector.pyandmixins/vector.pyare gone, as are the partition strategy mixins andtestsuiteproviderpartition.py. A dialect must now composeClickHouseColumnTypeMixinandClickHouseTrimMixin, declareColumnTypeSupport, and keep each ahead of the core class it overrides so C3 gives the backend's answer priority.ClickHouseTypeSupportmoved toprotocols/type.pyand must precedeDataTypeSupportin the base list for the same reason.Related Repositories/Packages
python-activerecordrelease/v1.0.0.dev30actions/checkoutfor core already points at itpython-activerecord-testsuitetest/typed-column-contractstest.ymlis intentionally left on this branchThe 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 ownfeature/typed-columnsbranch and each follows the same order: core, then testsuite, then the backends.Testing
workflow_dispatchruns 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 ClickHouseruns all five Python / ClickHouse pairs, and each of them installs the core fromrelease/v1.0.0.dev30and the testsuite fromtest/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 includefeature/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, andfeature/backend/introspection/(theSHOWoutput 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-connect1.10.0 declaresRequires-Python: <3.15,>=3.10, so this package'srequires-pythonstays>=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 inrequirements-dev.txtmirror 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
README.md,docs/en_US,docs/zh_CN).changelog.d/<PR number>.<type>.mdonce this PR exists, because towncrier numbering binds a fragment to the real PR number.ALTER COLUMNdefault path inlines the literal, as ClickHouse requires a literal there, and that is unchanged from before).Additional Notes
Out of scope, recorded so reviewers do not expect it. The
DataTypefour-table rework is Phase 2b's subject and is not started here; neither is theformat_*annotation sweep beyond what this branch touched. The two rendering facts found while measuring (LENGTH returning bytes, JSON equality needingtoString) are recorded inmixins/column_type.py's docstring but are deliberately not acted on, because rendering belongs to that later phase.The broad first commit.
8f72db5is large (76 files) becauserelease/v1.0.0.dev1predates 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 corerelease/v1.0.0.dev30, andrelease/v1.0.0.dev1is not.