Repository navigation
feat(dialect): follow the core's column-layer rebuild — column types, node formatters, and the version gates - #62
Merged
Conversation
… column_data_type)
setup-python now ships Python 3.15.0, so the release-candidate pins are replaced with plain 3.15 (both the GIL and the free-threaded legs), and the exemptions that made the leg optional are removed: - the "(experimental)" tag in the job names, and the experimental job ids; - `continue-on-error: true`, which let the leg report failure without failing the run; - postgres' disable gate (`if:` set to false) and its comment. The remaining matrix entries are unchanged: every backend keeps a GIL and a free-threaded leg on 3.15, now on equal footing with the other interpreters.
psycopg does not support free-threaded builds -- it is tracked upstream as psycopg/psycopg#1095 -- so a free-threaded leg on this backend can only ever fail at install or at connection. The 3.15 job keeps its GIL leg only, which is what every other interpreter here already runs. The job name now carries the free-threaded marker. It renders empty today, but the matrix had two 3.15 legs that both displayed as "Python 3.15 / postgres 18": the GIL leg passed and the free-threaded leg failed, which read as one job that had simultaneously passed and failed. With one leg the collision is invisible; keeping the marker stops it recurring if a second leg is ever added.
… Python The benchmark job was pinned to 3.14 while the test matrix had already moved to 3.15, so the newest interpreter this package supports was never exercised under load. It now runs on 3.15 (core, postgres and mysql) -- job id, job name and the summary text included -- while the per-version test legs stay on their own interpreters. The README carried the Pydantic requirement as one coarse line. It now carries the actual pairing: 3.8 pins an exact pair (the last installable there), 3.13 and 3.13t pin the pair whose pydantic-core still has a wheel, and the rest stay open. The reason column records why the 3.13 row cannot simply track the newest release, and the free-threaded paragraphs now state per backend which legs exist and why.
…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.dev17 #62 +/- ##
========================================================
+ Coverage 75.42% 75.93% +0.51%
========================================================
Files 346 349 +3
Lines 16111 16343 +232
Branches 2741 2731 -10
========================================================
+ Hits 12151 12410 +259
+ Misses 2969 2952 -17
+ Partials 991 981 -10
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 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
This is the PostgreSQL side of the core column-layer rebuild: the branch that makes a column class a capability bundle, turns the bare
Columninto a reference rather than an operator surface, and moves trim/lpad/rpad/repeat from function names routed throughformat_function_callinto dedicated nodes. A dialect has to answer for that rebuild in five places, and this PR answers all five.The column-type protocol is now implemented rather than assumed. The new
mixins/column_type.pycarriesPOSTGRES_COLUMN_TYPES— the protocol's closed list of eighteen common Python types, every one of them answered — andPostgresColumnTypeMixin(ColumnTypeMixin), composed intoPostgresDialectnext toPostgresDataTypeMixin. Putting the two side by side is deliberate: one says which operations a value carries, the other says how it is spelled, and the protocol exists to keep them apart. Two answers moved with the rebuild rather than being edited:floatanddecimal.Decimalnow answerNumericColumn, because core removedFloatColumn/DecimalColumn/RealColumn/DoubleColumnand the numeric family is deliberately one class — PostgreSQL really does have distinctnumeric/real/double precisionfamilies, but that is a storage fact theDataTypelayer spells, not something an operation class can express. One answer is a deliberate refusal:datetime.timedeltaanswersNone, because this server has a realintervaltype and core has noIntervalColumnto name its operations with, and the previousNumericColumnanswer handed callerssqrt(interval '1 day')— which the engine refuses outright. A field declaredtimedeltanow fails to resolve with a message naming the fix (UseColumnType), which is the honest version of that gap.The four new nodes are composed, not overridden.
PostgresDialectnow mixes inTrimMixin,LpadMixin,RpadMixinandRepeatMixin. PostgreSQL spells all four natively —TRIM([BOTH|LEADING|TRAILING] [chars] FROM str),LPAD/RPAD(str, len, padstr),REPEAT(str, n)— so the shared defaults are the answer and noformat_*override is written. That absence is the point worth reviewing: the mixins are listed with the reason no override is needed, recorded in the class body rather than left for the next reader to re-derive.The tests follow the accessor and naming rebuilds. The DDL-source accessors split:
column_type()is a declaration accessor, SQL-type reads go tocolumn_data_type()/columns_data_type()(), andcolumns_typeis gone.DateTimeColumnbecameTimestampColumn(the column layer models values, anddatetimeis a vendor spelling), andDateTimeSupportbecameTimestampSupport. Test code that built predicates through the bareColumn's removed operator sugar now builds them explicitly as nodes.The round-trip matrix was made load-bearing-aware. Core reports a missing formatter and a declined feature through the same
UnsupportedFeatureError, and this matrix allowed both identically — which let one of them hide. A class whose formatter this dialect never mixed in read as "PostgreSQL lacks the feature" and passed. Turning trim/lpad/rpad/repeat into nodes made the hole matter: a backend that shipped noformat_lpad_expressionwould answer every.lpad(...)with that refusal, and nothing could tell it apart from a genuine "not on this engine". The two are now told apart by who is refusing — a probe inside a formatter names the feature it lacks, while the dispatch frames the method name it could not find. Only the second is a wiring fact about this backend, so only the second has to be accounted for:UNMODELLED_FORMATTERSgroups the formatters PostgreSQL genuinely does not have (SQL:2003 PIVOT/UNPIVOT and the row-source root, today) with the reason, andtest_unmodelled_formatter_list_is_exactpins the table in both directions — a formatter that starts existing moves its classes out and the pin fails until the entry goes, and a class that starts needing an unlisted method fails instead of passing as "unsupported".The version gates were found empty by CI, four times over. A gate that returns True unconditionally tells the dialect it can render a type on any server, and the server disagrees at execution — the expensive way to find out, because the DDL is built, sent and rejected, and the error names a table rather than the version gap that caused it.
postgres_jsonpath(12),postgres_xid8(13), the six multiranges (14) andpostgres_macaddr8(10) were each found by CI, one at a time, and are now gated. Two more were simply wrong and are corrected with them:supports_jsonb_subscriptclaimed 14.0+ for a capability PostgreSQL does not have at all (SQL subscripting covers arrays and composites; jsonb is read with->/->>or the SQL/JSON path language), andsupports_json_arrow_operatorsreturned True unconditionally, which on 9.2 advertised SQL the parser rejects. The boundaries are measured against the scenario servers rather than read off the release notes, because the notes and the servers disagreed about jsonpath — the notes say 13, and 12 has it.KNOWN_VERSIONED_TYPESrecords every floor and the server it was read off, and the two test classes pin the two directions separately: a versioned type must not answer yes unconditionally, while text/integer/varchar saying yes is correct on purpose and is not a to-do list.Coincidental fixes found while aligning.
PostgresUUIDMixinandPostgresUUIDAdapterare new, registered withallow_overridebecause they claim the same{uuid.UUID: {str}}pair the core adapter does, and placed before core'sUUIDMixinso PostgreSQL's version-13-built-in-or-uuid-ossp answer wins over a single fixed spelling.PostgresArrayEnhancedMixinmoved from the backend block to beforeArrayMixin, where it answers True to a question the core answers False for — listed after, the core default was what every caller actually saw.JSONSupportis de-listed from the protocol block becausePostgresJSONBEnhancedSupportderives from it and listing a base ahead of its own subclass is not a consistent MRO (it also putsupports_json_typein the MRO four times);isinstance(dialect, JSONSupport)is unchanged. The twosupports_uuid_generationprobes that shared one name and asked two different questions — "is uuid-ossp installed" versus "can this server generate a UUID" — are separated:supports_uuid_ossp_extensionis the narrower question, and the merged answer lives only in core'sUUIDSupport, folded byPostgresUUIDMixin.format_cast_expressionrenders the target throughexpr.target_type.to_sql()instead of interpolating it, which had producedcol::IntegerType()rather thancol::INTEGER. AndPostgresTableMixin's data-type validation regex now accepts underscores, which no valid PostgreSQL type name was allowed to contain.The CI legs. Python 3.15 becomes a required leg rather than an experimental one: the previous
test-experimental-python-315job wasif: ${{ false }}withcontinue-on-error: trueon a 3.15.0-rc.2 pair. The free-threaded leg is dropped — psycopg does not support free-threaded builds (psycopg/psycopg#1095) — but the free-threaded marker stays in the job name, because two legs named alike is how a GIL pass and a free-threaded failure came to look like one job that both passed and failed. The benchmark job moves to 3.15.requirements-dev.txtpins pydantic 2.12.2 / pydantic-core 2.41.4 for Python 3.13 including the free-threaded 3.13t build, restated locally because that file is installed before the core package and an unpinnedpydanticthere picks a pydantic-core whose only cp313t distribution is an sdist that cannot build. CI installs the core from itsrelease/v1.0.0.dev30branch now that the rebuild has merged, and the testsuite fromtest/typed-column-contracts.Type of change
feat: A new featuredocsperftest: Adding missing tests and correcting existing tests for the new featurechoreci: Changes to CI configuration (Python 3.15 required leg, benchmark interpreter, Pydantic pins)build: Pydantic pinning for Python 3.13 / 3.13trevertBreaking Change
Applicable
rhosocial-activerecordVersion Range:>=1.0.0,<2.0.0— and, concretely, this branch requires the core at or above1.0.0.dev30, the release that carries the rebuild. It is not compatible with the previous core.Impact on End-Users — yes. This backend is consumed by people building on PostgreSQL-backed ActiveRecord, and the core's six end-user changes reach them through the dialect. The ones visible in this repo's own surface:
supports_data_type_postgres_jsonpathanswered True unconditionallyUnsupportedFeatureErroror target 12+supports_data_type_postgres_xid8postgres_*multirangeprobessupports_data_type_postgres_macaddr8supports_jsonb_subscriptclaimed 14.0+jsonb['key']; read with->/->>or the SQL/JSON path languagesupports_json_arrow_operatorsreturned True unconditionallyDateTimeColumn/DateTimeSupportnamesTimestampColumn/TimestampSupport; rename the importColumnlost its operatorsStringColumn,NumericColumn,ArrayColumn, …) or build the node explicitly (ComparisonPredicate,InPredicate,BinaryArithmeticExpression)DDLSource.column_type()is now a declaration accessorcolumn_data_type()/columns_data_type()();columns_typeis gonePostgresUuidOssSupport.supports_uuid_generationsupports_uuid_ossp_extensionfor the narrower question; asksupports_uuid_generation()for the merged answerPostgresCharacterVaryingType...impl.postgres.expressionFive of the rows above are the expected consequence of the repair rather than of the rebuild: those probes used to promise the wrong answer, and a caller that took them at their word rendered SQL the server rejects. The failure is now a refusal that names the version gap.
Impact on Backend Developers (Custom Implementations) — yes.
TrimMixin/LpadMixin/RpadMixin/RepeatMixin— or write aformat_*override where its spelling differs. This backend composes all four and writes no override; that is the case reviewers should check rather than assume.ColumnTypeSupportis a protocol a backend must implement. The shape is a table of common Python type → column class, withNoneas an allowed refusal, and a refusal has to be a considered answer:timedeltais refused here because no core column class namesinterval's operations, not because the entry was forgotten.format_*signature changes from core:expris annotated with its concrete expression type rather thanAnyor absent.PostgresUUIDMixinprecedesUUIDMixinandPostgresArrayEnhancedMixinnow precedesArrayMixin; and a derived protocol should not be listed alongside its base, which is whyJSONSupportis de-listed. Both were live bugs, not tidying.ColumnTypeSupportand, after the rename,TimestampSupport.Related Repositories/Packages
This change cannot be used alone; the coordinated branch in each repo is
feature/typed-columns(testsuite:test/typed-column-contracts).python-activerecordrelease/v1.0.0.dev30(#126)python-activerecord-testsuitetest/typed-column-contractspython-activerecord-{mysql,mariadb,oracle,sqlserver,firebird,clickhouse,snowflake,bigquery}feature/typed-columnsNote on the install references: this branch's CI installs the core from
@release/v1.0.0.dev30(its release branch, now that the rebuild has merged) and the testsuite from@test/typed-column-contracts. That testsuite reference is intentionally still a branch rather than a release — it is what makes this PR's test legs meaningful now, and it must be re-pointed to the testsuite's release branch oncetest/typed-column-contractshas landed, so the release branch does not keep depending on a branch that will be deleted.Testing
Test Plan:
Local verification is intentionally partial: this project forbids local full-suite runs, so CI is the gate. What answers for the whole is the GitHub Actions
test.ymlmatrix, which pairs Python 3.8–3.14 with PostgreSQL 9–18, adds the required Python 3.15 / PostgreSQL 18 leg and a PostgreSQL 19 leg on 3.14, and runs the FastAPI benchmark job on 3.15 / 18.Checklist
README.md).bool).self.version).Additional Notes
Two things a reviewer should not expect to find here. The
format_*annotations that core typed on its own side (44 of 691 org-wide) are not backfilled in this backend — the remaining ones are follow-up work, as they are in every backend. And thesupports_data_type_*probe family that core retired is not yet removed from this backend's declaration surface beyond the two probes noted above; the four backends that carry their own large probe sets (mysql, mariadb, firebird, clickhouse) are the shape of that rework, and this backend is not one of them.What the alignment surfaced, stated plainly. Six of the eleven rows in the breaking-change table are not consequences of the core rebuild but bugs it exposed: four probes that answered True for a server version that predates the type, one subscript PostgreSQL does not have at all, and one arrow operator on a version whose parser rejects it. Beyond the table, two MRO-order bugs surfaced the same way —
PostgresArrayEnhancedMixinlisted afterArrayMixinandPostgresUUIDMixinafterUUIDMixin, each of which made the backend's own answer dead code in favour of a core default. All of it is fixed here because the rebuild's tests are what found it, and the probe repairs are listed as breaking because a caller that took the old answer at its word rendered SQL the server rejects, where it now gets a refusal naming the version gap.