Repository navigation
Commit b6729ca
authored
feat(postgres)!: the catalogue as nodes, and one introspect question per family (#63)
* fix(types): refuse a non-boolean unsigned when the type is rendered
The shape check now lives where the flag has to be honoured rather than where
it is declared, so a value that would silently render the wrong DDL is named
instead. The attribute is read directly: whether the field exists is settled
by the class, so probing for it with a default would only turn a wrong class
into a quiet signed column.
CI installs the core library from fix/column-constraint-gates so this backend
runs against the branch it depends on.
* refactor(postgres): one class per storage, not two
Remove PostgresByteaType, PostgresUUIDType and PostgresXMLType. Each rendered
byte-identical SQL to the core class it derived from, and DataType.__eq__ is
type(self) is type(other) -- so a class differing only in its name made a
declaration and the catalog row it produced compare unequal, and the differ
reported an unchanged column as changed.
Unlike Firebird's, these were not unreachable: parse_type answered BYTEA, BLOB,
UUID and XML with the Postgres class rather than the concept, so the breakage
was live in both directions. It now answers with the core class, which is what
makes the round trip close -- each of the three comes back to itself, where
before it came back as a different class than it started as.
The round-trip test's WIDENING_ANSWERS loses three entries for that reason.
Those were not widenings: they recorded that parse_type disagreed with the
declaration, which is the defect.
* refactor(postgres): read declared fields directly instead of probing with getattr
Each of these sits behind a parameter whose annotation names the class, and
each field is in that class's constructor signature -- so nothing was ever in
doubt, and the default was only making a missing field look like a legal
answer.
* refactor(postgres): follow the statement and expression renames
Mechanical rename wave against the core's W1/W2/W3a/W3b/S1-c breaks.
Constraint surfaces follow in the next commit -- see below.
Migration surface (src + tests + docs, 215 files):
W2, 57 statement-level classes -> *Statement (core 34e1fb45). The
largest here: QueryExpression->QueryStatement 688, CreateTableExpression
->CreateTableStatement 415, DropTableExpression->DropTableStatement 309,
InsertExpression->InsertStatement 283, CreateIndexExpression
->CreateIndexStatement 86, UpdateExpression->UpdateStatement 39,
RefreshMaterializedViewExpression->RefreshMaterializedViewStatement 36,
TruncateExpression->TruncateStatement 36, CreateTableLikeExpression 36,
CreateMaterializedViewExpression 33, DropMaterializedViewExpression 32,
and the rest.
W3a, predicate/clause/match nodes (core 890aaaa3): ILIKEExpression
->ILikePredicate 29, FulltextMatchExpression->FulltextMatch 13.
W3b, value/function/introspection families (core 5dc64fd5):
WildcardExpression->Wildcard 30, SQLValue 18, QualifiedIdentifier 10,
JSONTextValue 10, Introspection*Info 53, CastFunction 8,
JSONDocumentValue 8, CaseFunction 3, CollateFunction 3,
UUIDCastFunction 3. UUIDCastExpression->UUIDCastFunction is the one
mapping the wave guide does not list; verified against the core commit.
W1, CTE family (core 683a183e): CTEExpression->CTEClause 24,
WithQueryExpression->WithStatement 20, and the call-site keyword
main_query=->query= 13 (every occurrence in this repo was an argument of
WithQueryExpression).
S1-c, PASSING: no change needed -- this backend constructs the XML
expressions through the xmlexists()/xmlquery() factories, whose passing=
signature is unchanged.
Override points renamed (functional; not renaming would drop the
override): format_ilike_predicate 5, format_begin_transaction_statement
2, format_cte_clause 1 (mixins/cte.py comment).
Behaviour: none. Every edit is a name; no SQL spelling moved.
Note: the constraint surfaces still reference the enums core's S3
removed (ColumnConstraintType/TableConstraintType), so this commit is an
expected red window -- the whole package fails to import until the next
commit. Wave 2 migrates the construction sites onto the branch nodes.
Verification:
- import smoke over the postgres backend modules: 147 import clean, 13
fail and all 13 share the single deleted-symbol cause (cannot import
name 'TableConstraintType') raised from
expression/ddl/exclude_constraint.py, which is a wave-2 surface.
- python -m compileall src tests docs: exit 0 (only the two pre-existing
SyntaxWarnings in docs/examples/chapter_02_connection_pool).
- ruff check on the 215 changed files: identical to the HEAD baseline
for the same file set (one pre-existing E501 in
test_insert_on_conflict_clauses.py resolved by the rename). No new
finding outside mixins/ddl/constraint.py, whose five F821s are the
known draft middle state.
* refactor(postgres): migrate constraints onto the two-level branch nodes
Core's S3 wave replaced the fat constraint nodes with a production plus a
branch: ``ColumnConstraint(branch=...)`` / ``TableConstraint(branch=...)``
name the branch, the branch names the kind, and the ``*ConstraintType`` enums
are gone. This commit migrates every construction site, every override and
every assertion in the package onto that shape, and absorbs the mechanical
part of the B-series draft that was sitting uncommitted in the worktree.
Construction sites migrated (src + tests + docs, 96 files):
ColumnPrimaryKeyConstraint 147 ColumnDefaultConstraint 39
ColumnNotNullConstraint 131 ColumnForeignKeyConstraint 8
ColumnNullableConstraint 4 ColumnUniqueConstraint 2
ColumnCheckConstraint 1 TablePrimaryKeyConstraint 7
TableForeignKeyConstraint 2 TableUniqueConstraint 1
TableCheckConstraint 1
Call shape: ``ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)``
becomes ``ColumnConstraint(dialect, branch=ColumnNotNullConstraint(dialect))``.
The old keyword-per-field form is gone with it -- ``default_value=`` is now the
branch's positional ``value``, ``foreign_key_reference=`` the branch's
``references`` pair, ``check_condition=`` the CHECK branch's ``condition``.
GeneratedColumnExpression is deleted from core, so its one call site became
``ColumnGeneratedConstraint``.
EXCLUDE. PostgreSQL's grammar is richer than core's ``TableExcludeConstraint``
(one operator per element plus a partial predicate), so the branch is
``PostgresExcludeBranch`` and ``PostgresExcludeConstraint`` becomes the
production-level sugar over it -- the same shape core's own
``ForeignKeyConstraint`` has, so the 20-odd call sites keep working unchanged.
``_format_exclude_constraint`` (private, production-and-body in one) became the
public ``format_table_exclude_constraint``, which now renders the body only:
the ``CONSTRAINT <name>`` prefix and the traits are the production's and are
rendered by ``format_table_constraint``, where the grammar puts them.
Override points:
- ``format_table_constraint`` -- narrows the enforcement trait to CHECK and
FOREIGN KEY (measured 0A000 for PRIMARY KEY/UNIQUE) before delegating.
- ``format_alter_constraint_action`` -- gates the verb on the measured
per-kind boundary (FOREIGN KEY from 18, CHECK from 19).
- ``format_column_constraint`` -- refuses the enforcement trait outright.
- ``format_add_table_constraint_action`` -- **removed**. Core already renders
the production, dispatches to the branch (so EXCLUDE gets its own formatter)
and appends NOT VALID; the only PostgreSQL-specific judgement was the
enforcement one and it lives in format_table_constraint. This is the
narrowing the guide asks for: fewer overrides, not more.
- ``_format_constraint_enforcement`` is deliberately NOT overridden. It is
shared by the column-level trait renderer and the table-level one, so an
override there would have to tell the two families apart by hand; the public
column-level seam says which family it is.
Behaviour changes (all measured, all previously silent or unimplemented):
- ``[NOT] ENFORCED`` on a column constraint now refuses. Measured 42601
``misplaced NOT ENFORCED clause`` for every kind on 18/19.
- A string or enum where a branch class belongs is a TypeError at
construction, not a tag that happens to be accepted.
- ``AlterConstraint`` takes ``constraint_kind=`` (branch class or spelling)
instead of ``constraint_type=``.
- EXCLUDE refuses DEFERRABLE/INITIALLY/ENFORCED: core's branch whitelist does
not list it and this backend does not widen it, so the request cannot be
written. ``PostgresExcludeConstraint(..., enforced=True)`` is a TypeError.
Serialization. ``PostgresAlterConstraint`` gains a ``constraint_kind`` view:
core's ``AlterConstraint`` declares ``constraint_kind`` but stores it as
``constraint_branch``, and ``get_params()`` -- the single serialization path --
resolves parameters by name, so without the view the kind is dropped and the
action cannot be rebuilt. The view yields the *spelling*, because a class
object is not encodable by any of the three channels.
Wildcard. ``functions/vector_search.py`` took ``Wildcard`` from core, which the
split removed, and the package would not import without it. That one import and
its one call site move to ``AllColumnsWildcard`` (a select-list position) here;
the rest of the split is the next commit.
Tests rewritten to the new API: the per-kind enforcement probe tests became
probe-version-gate plus formatter-behaviour; the column-level "renders
successfully" assertion became a refusal (the draft contradicted its own
docstring evidence -- pattern 10 of the ten reconciliation rules says the
assertion moves, not the measurement); the EXCLUDE deferral pair guards became
construction-refusal tests with the measurement quoted; ``format_default_
constraint`` became ``format_column_default_constraint`` on the branch.
Verification:
- import smoke over every module in the backend package: 346 imported, 0
failures (was 147 ok / 13 failing on the deleted enums at the previous
commit).
- targeted constraint suites, exit 0:
tests/.../postgres/test_ddl_alter_config.py
tests/.../postgres/test_postgres_constraint_capabilities.py
tests/.../test_postgres_dialect_security.py
tests/.../test_clause_pair_guard.py
tests/.../postgres/test_ddl_source_collection.py
tests/.../test_expression_roundtrip_all.py (472 passed, 3 skipped)
- ruff check on the 102 changed files: one finding below the HEAD baseline for
the same file set and no new class of finding. The one new F821 is one more
reference to the already-undefined `dialect` in
tests/.../extensions/test_pgvector_integration.py, a file with six such
findings at HEAD.
Not green, and not this repository's to fix: the testsuite's shared
feature/ddl contract cannot run, because
python-activerecord-testsuite@fix/column-constraint-gates (a57f266) still
imports the deleted ``ColumnConstraintType`` in
feature/ddl/{fixtures/models,test_derivation,test_derivation_async,
test_ddl_source_contract}.py. That is WP-T/S4 work in the testsuite repo.
* fix(postgres): converge the constraint switches onto the (self) probe protocol
The probes now say what they can answer and the formatters say the rest.
**Probes.** ``supports_constraint_enforced()`` and
``supports_alter_constraint_enforced()`` take no argument. A probe answers one
question -- "is this capability family present on this server" -- and the kind
is not part of that question; a parameterised probe has two call shapes
depending on its own signature, which is how the draft ended up with a switch
whose kind-less answer (``False`` on 19!) contradicted its kinded one. Both now
answer the version gate they were always answering:
supports_constraint_enforced() version >= 18 (170011 / 180006 / 190000)
supports_alter_constraint_enforced() version >= 18
The per-kind answers did not disappear; they moved to the place that can see
the kind, which is the branch class:
_require_constraint_enforced(branch) TableCheckConstraint / TableForeignKeyConstraint
and version >= 18; anything else refuses
_require_enforceable_constraint(branch) TableForeignKeyConstraint from 18,
TableCheckConstraint from 19
Both raise TypeError on a non-branch argument (pattern 4: a spelling is a type
question) and UnsupportedFeatureError naming the SQL spelling -- ``CHECK NOT
ENFORCED``, ``ALTER CONSTRAINT CHECK NOT ENFORCED`` -- with the message the
other ten backends use (pattern 7).
**Narrowing seams.** No private formatter was added, and the shared private
hook was left alone:
- column-level refusal -> the public ``format_column_constraint``, before
delegation. ``_format_constraint_enforcement`` is shared by the column-level
trait renderer and the table-level one, so an override there would have to
tell the two families apart by hand; overriding the column seam makes the
family the override's own business.
- table-level per-kind refusal -> the public ``format_table_constraint``.
- ALTER per-kind version -> the public ``format_alter_constraint_action``.
- EXCLUDE -> the public ``format_table_exclude_constraint``.
**Local declaration discipline** (pattern 5), now enforced rather than
asserted in prose. New in
test_postgres_constraint_capabilities.py:
- ``test_resolved_switch_is_owned_by_this_backend`` -- for every switch this
backend relies on, the first class in the dialect's MRO that has the name in
``__dict__`` must be inside this package. A core default that reaches the MRO
first would otherwise answer instead of the measured value, silently.
- ``test_the_declaring_class_is_a_postgres_mixin`` -- the answering class is a
PostgreSQL one, whatever mixin owns it. (Not every switch is on
``PostgresConstraintMixin``: ``supports_drop_constraint_if_exists`` is
declared by the ALTER COLUMN modifier, which reaches the MRO first. The
guard is therefore package-scoped, which is what the rule actually says.)
- ``test_probes_take_no_argument`` -- ``(self)`` and nothing else, checked on
the bound method so a defaulted parameter cannot slip back in.
- ``test_version_gate_matches_the_measurement`` -- the 17/18/19 boundary.
- ``test_docstring_quotes_the_measurement`` -- pattern 6, made mechanical: a
switch docstring must contain a SQLSTATE, a server version, or the server's
own "accepted" verdict. A docstring that stops quoting its measurement fails
here instead of quietly becoming a guess with a name.
**Docstrings.** The two "KNOWN DEFECT (core S3, deliberately not fixed here)"
paragraphs are gone -- the defect they described was core's
``inspect.signature`` sniffing of a parameterised probe, which core removed.
Each probe now points at where its per-kind half lives. The two stray
"Core S3 may rename it" notes are reworded against what S3 actually did
(moved constraint *nodes*, not the REFERENCES clause and not
``supports_rename_constraint``).
**Measured facts re-asserted, not re-derived** (pattern 10). Every SQLSTATE in
the new text was already in the draft's evidence and is quoted verbatim:
170011 42601 the clause is not in the grammar, any kind, any statement
180006 0A000 PRIMARY KEY / UNIQUE constraints cannot be marked NOT ENFORCED
180006 42809 cannot alter enforceability ... Only foreign key and check
190000 accepted for both statements, both kinds
column level, all versions: 42601 misplaced NOT ENFORCED clause
Verification:
- tests/.../postgres/test_postgres_constraint_capabilities.py, exit 0.
- tests/.../postgres/test_ddl_alter_config.py, exit 0.
- tests/.../test_clause_pair_guard.py, exit 0.
- ruff on the changed files: clean.
* refactor(postgres): split the wildcard by grammar position
Core split the single asterisk into two by where it sits in the grammar
(spec 4.10): ``AllColumnsWildcard`` for the select list and RETURNING,
``RowCountWildcard`` for ``COUNT``'s argument. The two answer different
questions -- ``COUNT(*)`` counts rows, ``t.*`` expands columns -- and only the
aggregate's contract admits the second.
Call sites migrated by position, not by name:
select-list / RETURNING (13) Wildcard -> AllColumnsWildcard
COUNT argument (3) Wildcard -> RowCountWildcard
examples/ddl/materialized_view.py, examples/query/pagination.py,
test_partition_operations.py
One qualified form changed shape as well as name:
``Wildcard(dialect, table="users", schema_name="app")`` became
``AllColumnsWildcard(dialect, relation=NamedRelationRef(...))``. The
qualification rides the relation object because the name to render is the name
the query uses, and that is what the reference holds.
Backend-specific: ``PostgreSQLRowCountWildcard``. ``COUNT(t.*)`` counts the
non-NULL rows of one relation rather than the rows of the join -- under a LEFT
JOIN the two answer differently -- so it is a separate node and not a flag.
Core's ``RowCountWildcard`` accepts no relation by contract, so the derivation
lives here and the override is ``format_row_count_wildcard``. The override
tests the class rather than probing for a missing attribute: core's node has no
``relation`` at all, and asking for one anyway is the reflection rule 9.8
forbids. Anything that is not the qualified form renders the bare asterisk, so
core's ``COUNT(*)`` path is untouched.
No ``format_wildcard`` override existed here, so none needed renaming.
Tests: ``test_schema_namespace.py`` gains the two wildcard contracts -- bare
``COUNT(*)`` stays bare, and ``COUNT(t.*)`` renders the alias the query uses.
Verification:
- import smoke over every module in the backend package: 347 imported, 0
failures.
- tests/.../postgres/test_schema_namespace.py, exit 0.
- tests/.../test_partition_explain.py, exit 0.
- tests/.../test_expression_roundtrip_all.py, exit 0.
- python -m compileall src tests docs: exit 0.
- ruff on the changed files: clean.
* ci: point testsuite at the matching branch
Every install line in .github/workflows/test.yml (four jobs: sync, async,
scenarios, lint) pinned the testsuite to its release branch while the core
line already pointed at fix/column-constraint-gates. The two have to move
together: the release branch's testsuite still builds constraints the way the
fat nodes were built, so against this backend's two-level nodes it would fail
at import.
rhosocial-activerecord-testsuite @ .../python-activerecord-testsuite.git
@release/v1.0.0.dev17 -> @fix/column-constraint-gates (4 places)
Checked and already correct, so untouched: the core install line is
git+https://github.com/rhosocial/python-activerecord.git@fix/column-constraint-gates
at all four places. No other workflow installs either dependency by ref
(publish.yml and changelog-check.yml have no such line).
No test evidence: a workflow-file ref change has nothing to run against. The
import smoke over the backend package (347 modules, 0 failures) is what stands
behind the branch pairing it points at.
* fix(postgres): name the protocol's column-constraint parameter as the mixin does
The signature-conformance test compares the protocol's declared parameters
with the mixin's, and they had to agree: the protocol said \�xpr\, the
override said \constraint\ (which is also what core's own
\ormat_column_constraint\ calls it). Fixed the protocol, not the
override, because the override's name matches the one it overrides.
Caught by tests/.../test_postgres_protocol_conformance.py, exit 1 before,
exit 0 after.
* fix(postgres): read the auto-increment flag where the two-level split put it
The identity injection in format_column_definition still probed the column-constraint clause for is_auto_increment with getattr -- a 9.8 reflection on a field that now lives on the PRIMARY KEY branch, so the probe always answered False and every auto-increment declaration was silently dropped from CREATE TABLE. Read constraint.branch.is_auto_increment behind an isinstance of ColumnPrimaryKeyConstraint instead.
The two DDL tests had migrated the branch but left the flag on the clause (TypeError: unexpected keyword argument), which is how the CI matrix caught it: 11 failed / 9944 passed at b52c8a9, all in test_auto_increment_ddl and test_identity_execution.
Targeted: both files exit 0 on the central venv.
* docs(postgres): carry the renames into the README and docs
* refactor(expression): follow the core renames and the word-form contracts
Carry the W3c class renames, the R1 signature alignment and the R2a arithmetic/concatenation renaming through the backend sources, examples and tests. The roundtrip matrix needed the two new core classes registered (ArithmeticOperation, SetOperation) and three imports that the rename left dangling.
* refactor(postgres): read declared fields instead of probing with getattr
Carry the core S2 discipline (101f71a5) through the 26 remaining probe
sites, following the fcc9939 precedent. Three shapes were needed:
* direct read (form A) where the field is declared on the node the
annotation names: ddl_table.py partition/table_options/temporary,
ddl/type.py definition_kind/action_kind (abstract properties on
TypeDefinition/TypeAlterAction), ddl/domain.py action_kind
(DomainAlterAction), materialized_view.py concurrent
(RefreshMaterializedViewStatement) and action_kind
(MaterializedViewAlterAction), types/jsonb_enhanced.py mode
(JSONDocumentValue declares it, JSONTextValue inherits);
* isinstance dispatch where the field exists only on the PostgreSQL
subclass: DROP DOMAIN options and CREATE INDEX opclasses /
nulls_not_distinct / with_options, DROP TYPE cascade/restrict, CREATE
MATERIALIZED VIEW if_not_exists -- the core nodes carry none of these
and now say so with a False branch instead of a silent default;
* enum-or-spelling (form C, core ddl_comment.py:72 pattern):
comment.py object_type, copy.py on_error/log_verbosity now branch on
isinstance(x, Enum). type_values/text_search.py judges the
to_postgres_string protocol by isinstance(TsQueryNode) instead of
hasattr.
Migration face: 26 getattr/hasattr probes -> 0 (static gate
'rg (getattr|hasattr)\(\s*(expr|node|clause|item)\b src' is now empty).
Behaviour tightening: an object that reaches the ALTER DOMAIN / ALTER
TYPE / MATERIALIZED VIEW fall-throughs without the declared
action_kind, or a JSONPath node without mode, now fails with
AttributeError instead of rendering the class name into the refusal
message. No in-repo caller hit the removed branches.
Also dropped two dead imports in mixins/ddl/type.py (SchemaObject,
shadowed typing.Type) so the touched file is ruff-clean; both were
flagged on HEAD already.
Tests (central venv, PYTHONPATH=src;tests, all exit 0):
postgres/test_index_pg.py, dialect/test_materialized_view.py,
postgres/test_json_path_modes.py, postgres/test_textsearch_types.py,
postgres/test_ddl_expressions.py, postgres/test_pg19_copy_repack.py,
postgres/test_ddl_objects.py, postgres/test_ddl_protocols.py,
postgres/test_enum_types.py, dialect/test_materialized_view_docs.py,
postgres/test_ddl_alter_config.py, test_partition_operations.py,
test_postgres_protocol_conformance.py, postgres/test_ddl_source_collection.py.
Pre-existing, unrelated: test_clause_pair_guard.py
TestSetOperationWordsAreClosed[union_all*] fails on HEAD 8f56c2c with a
substring assertion ('UNION' is contained in 'UNION ALL'); to be repaired
with the set-operation follow-up.
* refactor(postgres): follow the core's kind-based artifact registry
Core ee5f4b55 "give an artifact a kind, not a class path" removed
register_expression_class from backend.expression.serialization: an
artifact is now a bare {kind, params} node, the kind is computed by
BaseExpression.kind(), and deserialization resolves kinds through a
registry whitelist the framework populates by walking its own packages
(core, sqlite, dummy). The backend-side registration module this
repository kept for the old FQN index is therefore dead code, and after
that core commit it broke the whole backend import chain (dialect ->
mixins -> expression/__init__ -> registry).
Removed expression/registry.py and its import-time call from
expression/__init__.py, following the shape mysql already converged to
(no backend-side registration module; whoever reads an artifact back
enters the kinds it names, which is the core commit's own answer to
"who registers a backend's kinds").
Same core commit, test side: the two TYPE-definition round-trip tests in
postgres/test_ddl_objects.py deserialize PostgreSQL-specific kinds, which
the framework whitelist does not carry. The test module now registers
exactly what it reads back -- the two definition classes, the composite
attribute dataclass and the UNSET sentinel enum -- into the default
registry at import time. Also dropped the pre-existing unused Literal
import so the touched file is ruff-clean.
Behaviour change: PostgreSQL-specific expression classes are no longer
entered into the (removed) global deserialization index at import time.
Serializing them was never affected - a node carries its own kind.
Test evidence (central venv, PYTHONPATH=src;tests):
- import smoke `from ...postgres.dialect import PostgresDialect` exit 0
(raised ImportError before the removal);
- postgres/test_ddl_objects.py exit 0 (the two round-trip tests failed
with "kind ... is not in the kind whitelist" before the registration);
- postgres/test_typed_values.py (new, next commit) and the
-k "typed or literal" selection both exit 0.
* feat(postgres): adopt the typed value nodes with PostgreSQL literal spellings
Wire the core TypedValueMixin (473b8e37 + 5ba491d5 + 1549380b) into the
dialect: a new PostgresTypedValueMixin rides ahead of it in the base
list, so the twelve typed value nodes resolve per kind while exactly the
three PostgreSQL spells differently stay backend-owned (spec 4.4 tier 2).
Overridden, with the inline (DDL-position) spellings:
- format_binary_value: '\x0102'::bytea -- the hex bytea literal, rendered
from the bytes the node holds. Hex input needs PostgreSQL 9.0+; every
declared version satisfies that.
- format_uuid_value: '...'::uuid -- the quoted canonical form plus the
cast, the same shape the dialect's UUID_SQL table spells for the UUID
constants (mixins/uuid.py).
- format_interval_value: INTERVAL '...' with the span decomposed into
days / hours / minutes / seconds, every emitted component carrying the
sign. Per-component signs are a measured answer, not a preference:
live servers parse INTERVAL '-2 days 03:04:05.000006' as minus two
days PLUS 03:04:05 (the leading minus does not carry into the time
part), so signing each component is the only faithful spelling. A zero
span renders INTERVAL '0 seconds'; microseconds render as a six-digit
fraction when present.
Tier-1 audit: PostgreSQL agrees with the base defaults everywhere -- TRUE
/ FALSE booleans, numeric text, quoted ISO dates and times, doubled-quote
strings -- so no tier-1 method is overridden and no literal logic copied.
The data position binds bytes / uuid.UUID / timedelta through the core
_bind_value; the psycopg placeholder is left to the dialect base.
Measured live 2026-10-17/18 against servers reporting 13.23, 17.11,
18.6 and 19beta4 (Debian): all four accept the three spellings, and a
CREATE TABLE whose bytea / uuid / interval columns carry them as DEFAULT
values round-trips exactly through INSERT ... DEFAULT VALUES + SELECT on
all four (oldest scenario configured as "postgres_9" measured 13.23).
New test file postgres/test_typed_values.py: MRO pin (the three methods
resolve on the postgres mixin), data-position bindings for every
overridden kind plus tier-1 passthrough, exact DDL-position texts for
binary (bytes and bytearray), uuid and seven interval shapes, and live
sentinel + column-default read-backs on every configured scenario.
Test evidence (central venv, PYTHONPATH=src;tests):
- postgres/test_typed_values.py: 61 executions exit 0
(17 offline + 4 live across 11 configured scenarios);
- feature/backend -k "typed or literal" (5 wave-4-scope collection
breakages from core's in-flight commits ignored, see report): exit 0;
- ruff clean on every touched file (also fixed the pre-existing missing
PostgresObjectNameMixin __all__ entry flagged in mixins/__init__.py).
Open items, not this wave: the roundtrip matrix file still imports the
retired serialization API and the testsuite expression helper -- wave 4
reconciles it against the core HEAD of that day, mirroring mysql's
vendored harness.
* fix(postgres): follow the classed function entry points
Core 9b559030 classed the aggregates and the scalar string functions,
a3978e10 the math family, and c5210245 reserved the generic FunctionCall
for user-defined functions. The semantic sites in this repository that
the core has classed now build (or delegate to) those classed nodes; the
~295 remaining FunctionCall sites spell extension-specific functions the
core has not classed (postgis, hstore, pgcrypto, ll_to_earth, ...) and
stay generic on purpose -- FunctionCall is the UDF node, and migrating
an unclassed concept would chase the moving target the wave guide
forbids.
Migrated (functions/math_enhanced.py, 9 of 11 factories):
- round_ / trunc -> RoundFunction / TruncateFunction via for_operand,
preserving the always-rendered precision operand (default 0 binds a
literal, an expression-valued precision passes through -- the previous
factory wrapped a Literal in another Literal when handed one)
- pow / power -> PowerFunction. pow keeps its name but now writes the
canonical POWER: both spellings accepted, measured live 2026-10-18 on
17.11 (POW(2.0,10) and POWER(2.0,10) both answer 1024)
- sqrt -> SquareRootFunction, mod -> ModuloFunction, ceil -> CeilFunction,
floor -> FloorFunction, avg -> AvgFunction (all render the same text
as before)
- max_ / min_ stay FunctionCall: GREATEST / LEAST are scalar and unclassed
even in the core (its own greatest/least factories also build the
generic node); the aggregate Min/MaxFunction are different concepts
Spelling override added (mixins/expression.py):
- format_truncate_function writes TRUNC. The core node spells the
SQL-standard TRUNCATE, which in PostgreSQL is the DDL statement that
empties a table; measured live 2026-10-18 on 17.11: TRUNC(3.716, 2)
answers 3.71, TRUNCATE(3.716) is refused with UndefinedFunction
"function truncate(numeric) does not exist"
char_length audit: PostgreSQL uses the core default. The core's
format_char_length_function writes LENGTH and deliberately does not read
the unit field; live-measured on 17.11, length('hello') = 5 characters
against octet_length('hello') = 6 bytes, so PostgreSQL's LENGTH already
answers the character question the core spelling assumes -- no override,
like oracle / snowflake / bigquery.
Leftover: examples/conftest.py described UNION as "using
SetOperationStatement"; the class is SetOperation. Also migrated the
ROUND site in examples/extensions/earthdistance.py to RoundFunction, and
fixed three pre-existing lint findings in the touched files (Table used
before its import in the earthdistance example, two overlong lines in
the math test).
Kept as FunctionCall, with the reason where it is subtle:
- functions/bit_string.py length / bit_length / octet_length: PostgreSQL's
bit-string overloads, not the text semantics the core classes carry
- functions/geometric.py length: the geometric overload
- examples/query/window.py ROW_NUMBER / SUM with window_spec: the classed
nodes take no window clause; the core has not classed the windowed form
Test evidence (central venv, PYTHONPATH=src;tests):
- postgres/test_postgres_math_enhanced_functions.py
(POW assertions updated to the documented POWER spelling) +
test_dialect_function_support.py + examples_compile_test.py: 61
executions exit 0
- ruff clean on every touched file
* refactor(postgres): follow the literal architecture the core settled
Core removed TypedValueMixin and the twelve *Value classes, leaving one
Literal(dialect, value, inline) node whose inline text goes through
SQLDialectBase.format_literal -- three tiers, of which the second is
exactly the types a backend spells for itself. PostgreSQL has three:
binary, UUID and INTERVAL. The 2026-10-17/18 live measurements behind
them (13.23 / 17.11 / 18.6 / 19beta4, CREATE TABLE DEFAULT read back
byte-exactly, the per-component interval sign) are carried over unchanged
-- an architecture change does not invalidate a measured answer.
Literal mixin
- mixins/typed_value.py -> mixins/literal.py: PostgresLiteralValueMixin
overrides format_literal (bytes -> '\x0102'::bytea, UUID -> '...'::uuid,
timedelta -> INTERVAL '...'); everything else defers to super().
- First position in the dialect base list, ahead of SQLDialectBase: a mixin
placed after the class it overrides never wins the lookup.
- test_typed_values.py -> test_literal_values.py, rewritten around the
inline flag. Binding and inline assertions, the tier-1 audit, the base
refusal and the live read-backs are all kept.
Partition bounds
- _inline_partition_expression is gone. Whether a value inlines is the
caller's per-node decision, so the pass that copied a bound tree and
swapped every Literal for the DDL class is: a bound renders as composed,
a literal left binding is refused by name. Three tests that relied on the
old lift now say inline=True; one new test pins the refusal.
Action renames (guide 10.3, same batch)
- AddColumn/DropColumn/AddTableConstraint/DropTableConstraint/AddIndex/
AlterConstraint/ValidateConstraint/AlterColumn/RenameColumn ->
*Action, in src, tests and the two schema_diff examples.
- The suffix lists in the roundtrip matrix lost the entries that named the
removed classes; a suffix matching nothing registers nothing.
- examples/ddl/alter_table.py was missing the Table import its own code uses.
Plain-JSON artifacts (guide 12.1)
- register_dataclass / register_enum are gone from the core registry, and an
artifact carries no class path any more, so the two PostgreSQL parameters
that were host objects had to become values:
* PostgresCompositeTypeAttribute gains as_params/from_params and
PostgresCompositeTypeDefinition overrides get_params to emit the
attribute as a mapping. The constructor takes either form and stores the
dataclass (entry wide, storage normalized).
* _UnsetValue is now (str, Enum) with __str__ = str.__str__, so the
"clause not supplied" sentinel travels as the text "UNSET"; the
constructor normalises it back, without which every deserialized base
TYPE would claim a DEFAULT it never declared.
Tests: 1044 passed, exit 0, over the twelve backend feature files this
touches. ruff check clean on every changed file except two pre-existing
E501s (mixins/ddl/column.py:126, test_ddl_expressions.py:286), both present
at HEAD and untouched here.
* refactor(postgres): audit the kinds this backend overrides and owns
Sweep the backend's expression package against the org rule that a kind core
has already issued may only be overridden in rendering, never redeclared as a
node class, and that a kind core does not have is the backend's own -- named
with the backend identifier in front.
Found and fixed, per the lineage script (core 357 kinds; backend 167 -> 161
custom kinds, 0 overrides -- the count that changes here is the one made of
duplicate nodes):
* ILIKE. ``expression/ilike.py`` declared ``ILikePredicate`` under the same
``format_ilike_predicate`` entry core's node declares, so one concept had two
classes and the artifact could not say which it meant. The module is gone;
``PostgresILIKEMixin.format_ilike_predicate`` is the whole PostgreSQL side,
and the two test modules import core's node.
* ALTER/VALIDATE CONSTRAINT. ``ddl/constraint.py`` exported one class under four
names each -- zero call sites, no old name to be compatible with -- and the
VALIDATE one added nothing at all. ``PostgresValidateConstraint`` is deleted
(core's ``ValidateConstraintAction`` is what callers build); the ALTER one
keeps its one real addition, ``constraint_kind`` as a readable property that
makes ``get_params()`` resolve the parameter core stores under another name,
and is renamed ``PostgresAlterConstraintAction`` so the class carries the
suffix its formatter already had.
* ENUM type fragments. ``CreateEnumTypeExpression`` / ``DropEnumTypeExpression``
/ ``AlterEnumAddValueExpression`` were ``pass`` subclasses of the ``Postgres*``
classes -- same behaviour, a second kind, no backend prefix. Deleted; the
``Postgres*`` classes are the only spelling. ``EnumTypeNameExpression`` /
``EnumValuesExpression`` are real nodes but their kinds had no backend prefix
and their formatter names did not match them; renamed
``PostgresEnumTypeNameExpression`` / ``PostgresEnumValuesExpression`` with
``format_postgres_enum_*`` entries.
* PARTITION BY. ``PostgresPartitionClause`` was an empty subclass of core's
``PartitionClause``: no field, no formatter of its own, one more kind for one
concept. Removed; PostgreSQL renders core's clause.
* Compound-word kind. ``PostgreSQLRowCountWildcard`` derived
``postgre_sql_row_count_wildcard`` -- ``PostgreSQL`` splits into two words
under the class-name rule, while every other class here says ``Postgres``.
Renamed ``PostgresRowCountWildcard`` (kind ``postgres_row_count_wildcard``).
Audited and left alone: every ``Postgres*`` node that subclasses the core node
of its concept and adds a typed field (``PostgresAlterColumn.using``,
``PostgresExcludeBranch.elements``/``where``, the type definitions, the domain
actions) -- those are derived nodes with a formatter of their own, not
redeclarations. The dialect MRO was read end to end: no PostgreSQL mixin sits
behind a core class that would shadow it.
Test evidence: ``pytest`` on test_expression_signatures.py,
test_ilike_support.py, test_enum_types.py, test_ddl_alter_config.py,
test_ddl_expressions.py, test_ddl_source_collection.py, test_schema_namespace.py
-> exit 0. ruff on every touched file -> clean.
* test(postgres): reconcile the roundtrip matrix with the plain-JSON artifact
Reconcile the matrix against core at 550ae740, where an artifact is a bare
{kind, params} node: no envelope, no marker keys, values as JSON scalars, and
the kind is the class name derived (``Literal`` the one exception, naming its
fourteen value kinds by what they hold).
The harness is now this repository's own (``expression_roundtrip_harness.py``),
vendored when testsuite deleted the shared module; its comparison learns that a
``(str, Enum)`` member and the word it carries are the same recorded value,
because JSON has no enum and a constructor may legitimately keep the word.
Matrix changes:
* ``values.*`` rows are gone -- there is no class per value kind. In their place
``test_value_kind_plane`` walks all fourteen kinds: wire name, JSON payload
type (NaN/+-Inf as text, Decimal and timedelta as text, bytes as base64,
list vs tuple preserved, everything else a plain scalar) and the round-trip
through all three channels. The set is pinned in both directions against
``LiteralKind``, and a ``float_value`` payload that is not finite is asserted
to be refused by the value layer rather than rendered.
* The removed K1 classes leave no rows: ``PostgresPartitionClause``,
``PostgresValidateConstraint``, the three enum compat subclasses and the six
constraint aliases.
* Constructors added for classes the generic guess could not build for real
reasons (a closed word set, a field name, an inline DDL condition, a value
rather than text, a declared-result annotation wrapping a column), and for
``CustomType`` (its ``raw`` is keyword-only), so it leaves UNCONSTRUCTIBLE.
* ``_CODEC_GAPS`` / ``_CODEC_RENDER_GAPS`` pin what core's codec cannot carry or
cannot read back, per class and per channel with the exception type: the XML
codec writes a ``(str, Enum)`` with ``Enum.__str__`` (the member's *name*), a
dataclass parameter has no encoding at all (``XMLAttribute``, ``XMLForestItem``,
``XMLTableColumn``, ``ReferentialAction``, ``TriggerEvent``, ``ViewOptions``,
``GraphColumn``, ``GeneratedColumnType``), ``CreateTableCloneMode`` is a plain
Enum, and the two XML nodes' formatters read ``.value`` off a decoded word.
Nothing else may lose a parameter, and each entry is exact in both directions.
Backend defects the matrix surfaced, fixed here:
* ``PostgresCollationMixin`` read ``expr.collation_options``; core's node
declares ``schema``, so every COLLATE raised ``AttributeError``.
* ``format_alter_database_statement`` compared against
``AlterDatabaseAction.CONNECTION_LIMIT`` / ``.TABLESPACE``, which core does not
declare -- every ALTER DATABASE died before rendering. It now recognises
PostgreSQL's two extra actions by word, resolves a decoded word back to the
core member, delegates the rest to core's formatter explicitly (``super()``
reached the protocol stub and answered ``None``), and refuses an unknown action.
* ``mode is LoggingMode.LOGGED`` answered "not LOGGED" for a round-tripped word
and rendered the opposite setting; it compares and spells the word now, and
refuses an unknown one.
* ``PostgresAlterTableRlsExpression`` read ``expr.mode.value``, which is gone
after a decode.
* ``hstore_literal``/``ltree_literal``/``xid8_literal`` chained ``.cast()`` onto
a ``Literal``; casting is a ``SQLValue`` capability and ``Literal`` is the
value node, so they build a ``CastExpression`` over it.
* The four value wrappers declared ``family``, stored it as ``_family``, and no
code ever read it -- removed (rules §4.4: a parameter that changes nothing).
* Every PostgreSQL-owned enum is now ``(str, Enum)`` with
``__str__ = str.__str__`` (§11.3), so the word is what str() and the artifact
carry.
Test evidence: the full matrix plus the domain round-trip file -> exit 0
(``531 constructible, 2 pinned-unconstructible; 471 rendered, 37 unsupported,
20 non-render, 3 unmodelled``). ruff clean on every touched file.
* refactor(postgres): follow the rules the org settled for kinds
Kind hygiene (expression-kinds.md §三, expression-dialect §3, §4.4, §11.2):
* A backend's own kind is named for the backend. Renamed
``NetworkValueExpression`` / ``RangeValueExpression`` / ``HstoreValueExpression``
/ ``LtreeValueExpression`` -> ``Postgres*ValueExpression``, ``PartitionValue``
-> ``PostgresPartitionValue``, ``CreateMultirangeTypeExpression`` /
``MultirangeAggFunctionExpression`` -> ``Postgres*``, ``MaterializedViewAlterAction``
-> ``PostgresMaterializedViewAlterAction``, and the two private helpers
``_LegacySqlExpression`` / ``_UnsupportedDomainAction`` ->
``_PostgresLegacySqlExpression`` / ``_PostgresUnsupportedDomainAction``. Every
affected ``format_method`` follows its kind, so class name, kind and entry are
one identity.
* ``PostgresAlterColumn`` -> ``PostgresAlterColumnAction``: the action carries
the suffix its formatter (``format_alter_column_action``) and its base
(``AlterColumnAction``) already had.
* The four value wrappers declared ``family``, stored ``_family``, and nothing
read it. Removed -- a parameter that changes nothing is not a parameter.
Kind hygiene is now a test (``dialect/test_kind_hygiene.py``): no backend class
may claim a kind core issues; a backend-owned kind must be ``postgres_*``; a
declared node's formatter must be ``format_<kind>`` (with an explicit,
two-way-checked table for the families that share one entry); and every enum in
the package must be ``(str, Enum)`` whose ``str()`` is its value.
Test discipline (expression-dialect §12, §12.1):
* The live-connection half of the literal work moved out of
``test_literal_values.py`` into ``test_literal_values_integration.py``, on the
scenario-parameterised ``postgres_backend`` fixture. What stays is
construct-and-render only, with the measured facts (and where to re-measure
them) recorded in the docstring. ``conftest.py`` in the expression directory
makes ``-n auto --dist=loadgroup`` the default there.
Serialization obligations (guide §13):
* §13.1: ``format_binary_operator`` spells the operator through ``.symbol`` when
it is a ``ComparisonOperator`` -- ``expr.op`` is now the word (``EQ``), and the
character is what SQL wants. A plain string operator (pg_trgm, range) is used
as it is.
* §13.4: audit of every ``*_need_quote`` pass-through in this backend; all are
real bools, no change needed.
Test evidence: the kind-hygiene file, the literal files (expression and
integration), the matrix, ``test_expression_roundtrip_all``,
``test_postgres_dialect_security``, ``test_extension_type_operations``,
``test_ddl_expressions``, ``test_ddl_alter_config``, ``test_materialized_view``,
``test_alter_table_if_exists`` -> exit 0. ruff clean.
* test(postgres): move live-connection checks out of expression tests
The expression directory is construct-and-render only (expression-dialect §12):
no connection, no schema, no file. Two things there still opened one, and one
configuration still left parallelism to whoever remembered the flag.
* ``test_literal_values.py`` keeps the wiring, the data-position bindings and
the inline spellings, and nothing else. Its ``TestLiveLiteralRoundTrip`` --
the sentinel, the CREATE TABLE with the rendered DEFAULTs, the read-back
through ``INSERT ... DEFAULT VALUES`` -- moved to
``test_literal_values_integration.py`` beside it, where the
scenario-parameterised ``postgres_backend`` fixture already lives. The
measured interval facts stay in the docstring that recorded them, with a
pointer to where a re-measurement goes; the numbers did not change and are
not restated in two places.
* ``tests/.../backend/expression/conftest.py`` makes ``-n auto`` and
``--dist=loadgroup`` this directory's default, and ``pyproject.toml`` pins
``--dist=loadgroup`` for the suite: a case runs on its own only when it
carries an explicit ``xdist_group``, and the reason for that marker is
written where it is used. ``-n 0`` / ``-p no:xdist`` still opt out.
* ``xdist_group`` is declared as a marker rather than left to the plugin, so a
typo in its name fails instead of being ignored.
Also in this commit, for the same rule set: the backend's transaction mixin and
tests follow core's ``with_*`` setters (``expr.isolation_level()`` ->
``expr.with_isolation_level()`` and the three siblings), and the parse-contract
sweep reads ``parse_roundtrip_failures`` from this repository's vendored harness
now that the shared testsuite module is gone.
Test evidence: the expression directory (parallel default in effect),
``test_literal_values.py``,
``test_literal_values_integration.py`` (live; skipped where no scenario server
is configured), ``test_expressions_transaction.py``,
``test_transaction_advanced_features.py``, ``test_postgres_type_protocol.py``,
``test_ddl_alter_config.py``, ``test_object_kind_guards.py`` -> exit 0. Live
capture is out of this change's reach (no scenario server on this host), and
that is stated rather than implied by a green run.
* fix(postgres): write the locking clause from its phrase, not its word
``format_for_update_clause`` interpolated ``LockStrength.UPDATE`` straight into
the SQL, and after core split a member's *word* (what the artifact carries) from
the SQL it spells, that renders a bare ``update`` -- a clause PostgreSQL
rejects. ``LockStrength.phrase`` is where core keeps the spelling now, the same
way ``SortDirection.keyword`` holds the ordering keyword. Guide §13.2's rule,
applied to the one place this backend still read a wire word as SQL.
Test evidence: ``test_row_level_locks.py`` -> exit 0 (one skip, a version-gated
case that predates this change).
* fix(postgres): read the word an artifact carries, not the member it used to be
The same defect the ALTER DATABASE and RLS formatters had, in three more places:
a value that was a ``(str, Enum)`` member at construction comes back as its
*word* after a round-trip, and the formatter read ``.value`` off it.
* ``format_create_table_statement`` interpolated ``col_def.storage.value`` for
``STORAGE``.
* ``format_create_policy_statement`` interpolated ``cmd.value`` for the
``FOR <command>`` clause.
* ``format_create_domain_statement`` interpolated ``expr.nullability.value``
and passed the member to ``supports_domain_nullability``, whose set
membership would refuse the decoded word. The word is resolved back to its
member once, by name, so the probe keeps answering and an unknown word is
refused explicitly.
All three now interpolate the member (its ``__str__`` is its word, §11.3) and
resolve for capability checks.
Test evidence: ``test_domain_expression_roundtrip.py``, ``test_policy.py``,
``test_ddl_expressions.py``, ``test_ddl_source_collection.py``,
``test_expression_roundtrip_all.py`` -> exit 0. ruff clean.
* test(postgres): record the matrix reconciliation baseline
The matrix file's header now states what it is reconciled against (core
``550ae740``, the plain-``{kind, params}`` artifact), what the value-kind plane
assertion exists for, and the census it currently reports. That is the one
place a reader looks when the numbers move, so the numbers and the baseline
belong in it rather than only in a commit message.
Evidence: the matrix plus the domain round-trip file -> exit 0 (533
registered, 531 constructible, 471 rendered, 37 unsupported, 20 non-render,
3 unmodelled). ruff clean.
* docs(postgres): build a cast over the literal in the extension examples
The examples still chained ``Literal(...).cast(T)``. ``Literal`` is the value
node -- it answers "bind or inline" and nothing else -- so the cast is its own
node, built from the literal. The examples now show the shape the library
actually offers, which is the one a reader copies.
No behaviour is asserted by these files (they are illustrative fragments and
reference names that only exist inside an ``if installed`` block); they are
parsed, not executed, and that is unchanged.
* test(postgres): use the edge-table definition node the class is actually called
``EdgeTableDefinition`` is the CREATE PROPERTY GRAPH definition; ``EdgeTable``
is the catalogue object it names. The test imported the catalogue object under
the definition's name and constructed it with definition arguments, which only
passed while both were spelled similarly. The class was renamed for exactly this
reason, and the test follows.
Evidence: ``test_property_graph_query_format.py`` -> exit 0.
* test(postgres): walk core`s package instead of reading the shared registry
The kind-hygiene check compared this backend`s kinds against
``default_registry().node_kinds()``. That registry is process-global and the
matrix registers this backend`s classes into it, so the comparison answered
differently depending on which module imported first -- the test failed when the
matrix ran before it, and passed alone. Core`s own package is the stable source,
and walking it is the same mechanism the matrix already uses to decide what
counts as a kind.
Evidence: the matrix + domain round-trip + kind hygiene + the expression
directory in one run (matrix first) -> exit 0.
* fix(postgres): declare the domain formatters this backend serves
PostgresDomainMixin renders two nodes the core kinds do not carry: the legacy sql= argument and the ALTER DOMAIN action words PostgreSQL spells. Both reach the dialect through format_method, so each is a member the mixin serves -- and a protocol is the declaration of that surface (§7.1.1). Declared them on PostgresDomainSupport; the reverse-coverage conformance test is now clean.
Test evidence: test_postgres_protocol_conformance.py -> 559 passed (was 558 passed, 1 failed).
* fix(postgres): follow core's EdgeTable rename in the PGQ scenarios
Core split one name into two contracts: the relation object is EdgeTable in objects.relation, and the DDL node that declares an edge in CREATE PROPERTY GRAPH is EdgeTableDefinition in expression.graph (kind graph.edge_table_definition). The scenarios file still imported EdgeTable from the expression package, which no longer exports that name -- so the module failed to import at all, and the four DDL sites that carry source/destination keys would have been the definition node and the eleven GraphEdge sites the relation object.
Split them accordingly: EdgeTableDefinition where key lists are passed, and EdgeTableObject for GraphEdge, which is what core's format_graph_edge now type-checks (GraphEdge.table must be an EdgeTableObject).
Test evidence: module imports and compiles; the scenarios are capability-gated and inert without a live SQL/PGQ server -- the live-server assertions are recorded as 'to be verified in CI'.
* fix(postgres): follow core's EdgeTable rename in the harness and matrix
Core split one name into two contracts: the relation object is objects.relation.EdgeTable, and the CREATE PROPERTY GRAPH node is expression.graph.EdgeTableDefinition, kind graph.edge_table_definition. The vendored harness still registered the constructor under graph.EdgeTable and built the removed class, so the property-graph row could neither be constructed nor matched against the gap table; the registration now names the kind core issues and the declared-gap row is what the matrix expects.
Verified: test_expression_roundtrip_all.py -> 1072 passed, 4 skipped (the pins skip by design); ruff clean on both files.
* fix(postgres): build the matview list with the with_ prefix core renamed
Core turned include_system from a method on the introspection expression into a declared field with a with_include_system builder, the same way schema became a field with with_schema. The introspector still chained .include_system(...) as a call -- so every materialized-view listing that asked for system objects raised 'bool' object is not callable, silently if the caller swallowed it. Routed both chained sites through the builder.
Test evidence: tests/feature/backend/dialect -> 337 passed (was 336 passed, 1 failed), including test_list_query_can_include_system_objects. Ruff on the touched files matches the HEAD baseline (4 pre-existing B009, unchanged).
* fix(postgres): compose collate over a Literal with the factory, not a chain
The value nodes carry no operation surface any more (rules 4.3: a literal is
a value and nothing else, and a composition such as an explicit COLLATE is its
own node), so Literal(...).collate(...) is an AttributeError -- the chain lives
on SQLValue, which a Column is and a Literal is not. The literal test now
composes the CollateFunction through the collate() factory over the literal
node, which renders the same SQL and the same binding:
('POSIX', ('Alice',)). The Column tests are untouched -- a Column is an
SQLValue and keeps the chain.
Test evidence: the five SQL-generation bodies of TestPostgresCollationExpression
run green directly against a PostgresDialect(version=(15, 0, 0)); the
collate-executes leg is recorded as to be verified in CI. Ruff clean on the
changed file.
* fix(postgres): cast a Literal with the cast factory, not a chain
A Literal is a value node and carries no operation surface of its own (rules 4.3), so Literal(...).cast(t) is an AttributeError; the cast of a literal is composed with the cast() factory over the literal node. Column keeps its chain -- a Column is an SQLValue. Same rendered SQL, same bindings.
Test evidence: test_type_casting.py -> 31 passed (was 30 passed, 2 failed across the two Literal casts), ruff clean.
* fix(postgres): cast a Literal with the cast factory, not a chain
A Literal is a value node and carries no operation surface of its own (rules 4.3), so Literal(...).cast(t) is an AttributeError. The three sites that built a value for a VALUES cell and a chained conversion now go through the cast() factory, which renders the identical SQL and the identical bindings. The chained-cast loop starts at a Literal and lands on a CastExpression, so routing every step through the factory keeps one shape for the whole chain.
Test evidence: test_cast_integration.py -> 157 passed, 8 skipped (was 85 failed across the three ConstructionError groups), ruff clean.
* fix(postgres): build the extension-test values through the cast factory and the operator nodes
Literal carries no operation surface (rules 4.3), so the extension tests' Literal(...).cast(t) chains go through the cast() factory -- rendering and bindings unchanged. The PostgreSQL containment/overlap operators (@>, &&, <@) are operator symbols, not one of the six comparison words the artifact may carry, so they build BinaryExpression (the shape the sibling unit tests in test_cube.py already assert); the ltree AND combination builds LogicalPredicate.
Local evidence: extension area --collect-only exit 0; ruff delta zero semantic findings (the remaining two kinds shift line numbers only and exist on HEAD); the live-DB legs stay for CI.
* fix(postgres): give the aliased literals a typed path and the remaining casts the factory
A Literal is neither aliasable nor castable (rules 4.3): aliasing belongs to what produces a named result, which is SQLValue, so the CTE depth counters go through cast(dialect, value, IntegerType) with the alias on the cast; the remaining Literal(...).cast(t) sites (extension type execution, plus one constructed through pg.dialect) go through the same factory. Rendering is a named integer either way.
Local evidence: literal-chain scan is zero across src/tests/examples; the three touched test files plus the example collect clean (exit 0); the live-DB legs stay for CI. ruff clean on the touched files.
* fix(postgres): collect every expression parameter at construction, and name the operators again
Four defects the second CI run exposed, all of the same family -- an expression that does not have its parameters, or has them under a name that no longer exists:
- cube/intarray @> (and the remaining && / <@) build BinaryExpression: they are operator symbols, not one of the six comparison words the artifact may carry, and the unit tests beside them already asserted the binary shape.
- the materialized-view introspectors called .schema() and .with_include_system() as methods on nodes whose schema/include_system are constructor fields (core's fluent setters retired with the word-form split), so every materialized-view query raised TypeError: NoneType is not callable. They now pass the fields at construction.
- the BEGIN path chained with_isolation_level()/read_only()/with_deferrable() onto a fresh BeginTransactionStatement the same way. READ WRITE is PostgreSQL's default and was never rendered, so the mode field stays unset unless READ ONLY is asked for -- the rendered bytes are unchanged.
- the remaining Literal(...).cast(t) sites (a multiline multirange pair) go through the cast factory, and the cosine-similarity assertion reads ArithmeticOperation, which is what a word-form arithmetic node is.
- the introspection default-schema lookup read config fields through getattr with a constant name; they are read directly now.
Local evidence: pgvector dialect tests exit 0; the touched files collect clean; begin/mv render SQL compared byte-for-byte against the intended shapes; ruff clean on the touched set; the live-DB legs stay for CI.
* refactor(postgres): build the introspection catalog queries from their node parameters
Six formatters grew their pg_catalog queries with ``sql +=``: the base query was
an f-string, and every conditional branch appended another fragment to a string
that was already half built, so no single place showed the whole query and the
WHEN/ELSE chains for a parameter were scattered across the appends. Each
formatter now collects the conditions its node's parameters ask for and joins
them into the WHERE tail in one place, which is how the MySQL backend already
spells the same family (its ``format_table_list_query`` and friends collect
conditions and join them once).
Migrated formatters, each driven only by the parameters the core introspection
node already collects:
- format_table_list_query: include_system and table_type collect conditions
- format_column_info_query: include_hidden collects a condition
- format_view_list_query: include_system collects a condition
- format_materialized_view_list_query: include_system collects a condition
- format_trigger_list_query / format_trigger_info_query: the optional
table_name collects a condition and its bind parameter
Local evidence: the rendered SQL was compared byte-for-byte over 22 shapes
(every include_views / include_system / table_type / include_hidden /
table_name branch of the whole introspection family, plus the materialized
view queries); all 22 are identical, parameters included. The one oddity kept
on purpose is format_column_info_query repeating ``NOT a.attisdropped`` when
include_hidden is false: the base query already excludes dropped columns, the
repeat is what this query has always rendered, and it is now a named fragment
with a comment saying so rather than an anonymous append. ruff clean on the
touched file; the dialect-level materialized-view introspection tests and the
whole DB-free introspection suite are green; the live-DB legs stay for CI.
* refactor(postgres): build the database-size query through its expression node
Both storage-info paths spelled the statement at the call site --
``sql = f"SELECT pg_database_size({self.dialect.p()}) as size_bytes"`` in
get_storage_info, sync and async -- so the introspector assembled SQL text
instead of asking a node for it. Core issues no node for this query, so this
backend declares two:
- PostgresDatabaseSizeFunction (kind postgres_database_size_function) owns the
``pg_database_size(name)`` call and carries the database name as a bind
parameter. The generic FunctionCall is not used for it: core reserves that
node for user-defined functions, and a standard function the server ships is
a named class an artifact can resolve.
- PostgresDatabaseSizeQueryExpression (kind
postgres_database_size_query_expression) owns the row shape -- the SELECT and
the size_bytes alias callers read -- and renders the call through the
function node, so the spelling lives in one place and the row shape in
another.
get_storage_info now builds the node with the database name and executes what
it renders; the SQL text and its parameter are exactly what the f-string
produced ("SELECT pg_database_size(%s) as size_bytes", (db_name,)), compared
byte-for-byte.
Local evidence: the new dialect test file pins the function's call text and the
query's full statement, both plain and with a quote in the name; the status
introspector suite, the kind-hygiene and expression-namespace guards, and the
full expression roundtrip matrix (which discovers both new classes and
round-trips them through dict/JSON/XML) are green; ruff clean on the touched
set. The live-DB leg of get_storage_info stays for CI.
* refactor(postgres): let the status, enum and constraint text belong to expression nodes
Three catalog queries the execution layer used to spell for itself -- the status introspector held the pg_settings/pg_stat_activity/pg_database/pg_namespace/pg_roles text as inline literals and f-strings at the call site, the enum manager assembled its WHERE clauses by string concatenation, and the transaction manager wrote SET CONSTRAINTS with an f-string that interpolated the constraint name raw. A SQL string is a formatting artefact: the node collects the parameters, the dialect formatter writes the text, and the executing layer only constructs and runs.
New nodes (kind from the class name, formatter in PostgresIntrospectionCapabilityMixin): PostgresSettingsQuery, PostgresActivityCountQuery, PostgresShowStatement, PostgresDatabaseListQuery, PostgresSchemaStatsQuery, PostgresCurrentDatabaseQuery, PostgresRoleListQuery, plus PostgresEnumTypeExistsQuery/PostgresEnumTypeValuesQuery and PostgresSetConstraintsStatement. The settings query's name list and the enum lookups travel as bind values now; the SET CONSTRAINTS name goes through the dialect's identifier quoting, so a name carrying a quote can no longer break the statement or escape it; PostgresShowStatement validates its parameter at construction. formatter-type imports land under TYPE_CHECKING, and format_postgres_set_constraints_statement is declared in PostgresTransactionSupport so the reverse-coverage conformance test passes.
Rendered SQL is otherwise unchanged; the live-DB legs are covered by CI. Local: full offline area (dialect/expression/types/transaction/conformance/matrix) exit 0; new test_set_constraints_statement.py exit 0; ruff clean on the touched set.
* docs(postgres): spell the partition example through its node, and correct the node's stale docstring
The partition example still assembled CREATE TABLE ... PARTITION OF with an f-string at the call site. It now builds PostgresCreatePartitionExpression, whose formatter produces the standard FOR VALUES FROM/TO spelling the example was hand-writing. The node's own docstring example had drifted from what the formatter renders (a RANGE (...) shape no server accepts) and is corrected to the real text.
Local: collection exit 0; the touched files accept ruff; the example's live-DB legs are covered by CI.
* fix(postgres): pass the LIKE operator as its enum, not the SQL keyword
The LIKE family's operator became an …1 parent 646a251 commit b6729ca
407 files changed
Lines changed: 18356 additions & 8100 deletions
File tree
- .github/workflows
- changelog.d
- docs
- en_US
- ddl
- postgres_specific_features
- type_adapters
- examples
- chapter_04_ddl
- chapter_12_named_procedure/queries
- zh_CN
- ddl
- postgres_specific_features
- type_adapters
- src/rhosocial/activerecord/backend/impl/postgres
- adapters
- backend
- cli
- examples
- connection
- ddl
- delete
- extensions
- insert
- named_expressions
- named_migrations
- pgq
- query
- schema_diff
- sqlxml
- transaction
- types
- update
- expression
- advisory
- ddl
- functions
- introspection
- mixins
- ddl
- dml
- extensions
- types
- named_expressions
- protocols
- ddl
- dml
- extensions
- types
- type_values
- tests
- providers
- fixtures
- rhosocial/activerecord_postgres_test/feature
- backend
- dialect
- expression
- introspection
- postgres
- extensions
- transaction
- types
- basic
- fields
- fixtures
- transaction
- query/collation
- relation/fixtures
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
69 | 69 | | |
70 | 70 | | |
71 | 71 | | |
72 | | - | |
| 72 | + | |
73 | 73 | | |
74 | 74 | | |
75 | 75 | | |
| |||
284 | 284 | | |
285 | 285 | | |
286 | 286 | | |
287 | | - | |
| 287 | + | |
288 | 288 | | |
289 | 289 | | |
290 | 290 | | |
| |||
396 | 396 | | |
397 | 397 | | |
398 | 398 | | |
399 | | - | |
| 399 | + | |
400 | 400 | | |
401 | 401 | | |
402 | 402 | | |
| |||
459 | 459 | | |
460 | 460 | | |
461 | 461 | | |
462 | | - | |
| 462 | + | |
463 | 463 | | |
464 | 464 | | |
465 | 465 | | |
466 | 466 | | |
467 | | - | |
| 467 | + | |
468 | 468 | | |
469 | 469 | | |
470 | 470 | | |
| |||
494 | 494 | | |
495 | 495 | | |
496 | 496 | | |
497 | | - | |
| 497 | + | |
498 | 498 | | |
499 | 499 | | |
500 | 500 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
182 | 182 | | |
183 | 183 | | |
184 | 184 | | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
24 | 24 | | |
25 | 25 | | |
26 | 26 | | |
27 | | - | |
| 27 | + | |
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
| |||
42 | 42 | | |
43 | 43 | | |
44 | 44 | | |
45 | | - | |
| 45 | + | |
46 | 46 | | |
47 | 47 | | |
48 | | - | |
49 | 48 | | |
| 49 | + | |
50 | 50 | | |
51 | 51 | | |
52 | 52 | | |
| |||
81 | 81 | | |
82 | 82 | | |
83 | 83 | | |
84 | | - | |
| 84 | + | |
85 | 85 | | |
86 | 86 | | |
87 | 87 | | |
88 | 88 | | |
89 | 89 | | |
90 | | - | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
91 | 93 | | |
92 | 94 | | |
93 | 95 | | |
| |||
179 | 181 | | |
180 | 182 | | |
181 | 183 | | |
182 | | - | |
| 184 | + | |
183 | 185 | | |
184 | 186 | | |
185 | 187 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
9 | | - | |
10 | | - | |
11 | | - | |
12 | | - | |
13 | | - | |
14 | | - | |
15 | | - | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
16 | 16 | | |
17 | | - | |
18 | | - | |
| 17 | + | |
| 18 | + | |
19 | 19 | | |
20 | 20 | | |
21 | 21 | | |
| |||
30 | 30 | | |
31 | 31 | | |
32 | 32 | | |
33 | | - | |
| 33 | + | |
34 | 34 | | |
35 | 35 | | |
36 | 36 | | |
| |||
44 | 44 | | |
45 | 45 | | |
46 | 46 | | |
47 | | - | |
48 | | - | |
| 47 | + | |
| 48 | + | |
49 | 49 | | |
50 | 50 | | |
51 | 51 | | |
52 | | - | |
| 52 | + | |
53 | 53 | | |
54 | 54 | | |
55 | 55 | | |
56 | 56 | | |
57 | 57 | | |
58 | | - | |
| 58 | + | |
59 | 59 | | |
60 | 60 | | |
61 | 61 | | |
| |||
66 | 66 | | |
67 | 67 | | |
68 | 68 | | |
69 | | - | |
| 69 | + | |
70 | 70 | | |
71 | 71 | | |
72 | 72 | | |
| |||
82 | 82 | | |
83 | 83 | | |
84 | 84 | | |
85 | | - | |
| 85 | + | |
86 | 86 | | |
87 | 87 | | |
88 | 88 | | |
| |||
96 | 96 | | |
97 | 97 | | |
98 | 98 | | |
99 | | - | |
| 99 | + | |
100 | 100 | | |
101 | 101 | | |
102 | 102 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
16 | | - | |
17 | | - | |
| 16 | + | |
| 17 | + | |
18 | 18 | | |
19 | 19 | | |
20 | | - | |
21 | | - | |
| 20 | + | |
| 21 | + | |
22 | 22 | | |
23 | 23 | | |
24 | 24 | | |
25 | 25 | | |
26 | 26 | | |
27 | | - | |
| 27 | + | |
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
31 | 31 | | |
32 | 32 | | |
33 | | - | |
| 33 | + | |
34 | 34 | | |
35 | 35 | | |
36 | 36 | | |
| |||
117 | 117 | | |
118 | 118 | | |
119 | 119 | | |
120 | | - | |
| 120 | + | |
121 | 121 | | |
122 | 122 | | |
123 | 123 | | |
124 | | - | |
| 124 | + | |
125 | 125 | | |
126 | 126 | | |
127 | | - | |
| 127 | + | |
128 | 128 | | |
129 | 129 | | |
130 | 130 | | |
131 | 131 | | |
132 | | - | |
| 132 | + | |
133 | 133 | | |
134 | 134 | | |
135 | 135 | | |
| |||
145 | 145 | | |
146 | 146 | | |
147 | 147 | | |
148 | | - | |
| 148 | + | |
149 | 149 | | |
150 | 150 | | |
151 | 151 | | |
152 | 152 | | |
153 | 153 | | |
154 | 154 | | |
155 | 155 | | |
156 | | - | |
| 156 | + | |
157 | 157 | | |
158 | 158 | | |
159 | 159 | | |
| |||
209 | 209 | | |
210 | 210 | | |
211 | 211 | | |
212 | | - | |
| 212 | + | |
213 | 213 | | |
214 | 214 | | |
215 | 215 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
140 | 140 | | |
141 | 141 | | |
142 | 142 | | |
143 | | - | |
| 143 | + | |
144 | 144 | | |
145 | 145 | | |
146 | | - | |
| 146 | + | |
147 | 147 | | |
148 | 148 | | |
149 | 149 | | |
150 | | - | |
| 150 | + | |
151 | 151 | | |
152 | 152 | | |
153 | 153 | | |
154 | 154 | | |
155 | 155 | | |
156 | 156 | | |
157 | 157 | | |
158 | | - | |
| 158 | + | |
159 | 159 | | |
160 | 160 | | |
161 | 161 | | |
| |||
168 | 168 | | |
169 | 169 | | |
170 | 170 | | |
171 | | - | |
| 171 | + | |
172 | 172 | | |
173 | 173 | | |
174 | 174 | | |
175 | 175 | | |
176 | 176 | | |
177 | 177 | | |
178 | 178 | | |
179 | | - | |
| 179 | + | |
180 | 180 | | |
181 | 181 | | |
182 | 182 | | |
| |||
186 | 186 | | |
187 | 187 | | |
188 | 188 | | |
189 | | - | |
| 189 | + | |
190 | 190 | | |
191 | 191 | | |
192 | 192 | | |
| |||
207 | 207 | | |
208 | 208 | | |
209 | 209 | | |
210 | | - | |
| 210 | + | |
211 | 211 | | |
212 | 212 | | |
213 | 213 | | |
| |||
0 commit comments