Skip to content

fix(testsuite): resolve every gated protocol reference and build expressions from objects - #23

Merged
vistart merged 3 commits into
release/v1.0.0.dev17from
feature/expression-schema-object-refactor
Oct 8, 2026
Merged

vistart merged 3 commits into
release/v1.0.0.dev17from
feature/expression-schema-object-refactor

Conversation

@vistart

@vistart vistart commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Description

Core's expression / dialect separation landed as
rhosocial/python-activerecord#125 on
release/v1.0.0.dev30. This suite had to follow it, and two of the adaptations it needed failed
silently — in both cases the failure mode was a skip, not an error, so nothing in CI ever went
red and the coverage simply stopped existing.

Adds # (issue)

1. Protocol references that could not resolve, so the gated tests skipped

requires_protocol takes a dotted string, splits on the last dot and imports the result. Two forms
in this suite could not resolve:

  • the bare name "JoinSupport" and "SchemaSupport" — no dot at all, so rsplit raises
    ValueError and the resolver skips;
  • the stale dotted path ...dialect.protocols.IndexSupport, which stopped resolving when core's
    single protocols.py became a protocols/ package and the class was renamed.

Measured effect before the fix: the full-text-search compatibility test skipped 11 of 11
parametrisations, and every join test skipped, on every backend, with no failure to show for it.

Fixed at the call sites, not in the resolver: 44 protocol references — 41 inside
requires_protocol(...) across 15 files (bare JoinSupport ×28, bare SchemaSupport ×13) plus
3 occurrences of IndexSupport renamed to FulltextIndexSupport, one of which is the docstring
example in utils/common.py. The resolver itself is unchanged; it was doing exactly what it
documents.

2. A container annotation that reads as a relation name, so the class could not be built

Core's expression classes now take the object they act on rather than a parsed string, so the
fixtures construct them from objects. Where the shape allows, the fixture reads a parameter's
annotation and computes its text as the alias' own __name__; if that text mentions a relation word
— Table, View, Sequence and the rest — it hands the parameter a catalogue object.

List[str] was safe, because the container branch answered first. Sequence[X] was not:
typing.get_origin yields collections.abc.Sequence, the test for list/tuple/set misses it,
and what is left is the literal string "Sequence". A parameter asking for a list of columns
therefore received a Sequence object and the class could not be built at all.

_empty_container_for now answers for any parameterised alias before the relation-name test,
matching the collections.abc ABCs rather than the concrete types an annotation happens to name —
so Sequence[str], FrozenSet[str] and Mapping[str, int] are recognised as what they are. A
bare class annotation still returns None, so the relation test keeps seeing the parameters that
really do name an object.

Two further corrections in the same helper:

  • the relation-name test now uses word boundaries. TableConstraint and AddTableConstraint
    are constraint actions, not relations, and a substring test hands them a Table.
    SchemaObject is the base every catalogue object shares, so an annotation naming it wants an
    object too.
  • an INSERT's data source is an expression now, so it needs a real one — the generic placeholder
    cannot invent the rows a VALUES list is made of.

Where the introspective guess still cannot satisfy the shape, 26 explicit per-class constructors
were added (_create_sequence_expr, _alter_type_expr, _drop_domain_expr,
_create_property_graph_expr, …).

Measured, by walking every class's __init__ in the core expression package and re-deriving
what the old code decided: 58 parameters reached the relation test, 51 of them annotated
with a single class, and 7 wrongly annotated with a parameterised alias — columns, attributes,
actions, parts, items, keys and passing, all Sequence[...]. Against MariaDB those seven classes
went from none constructible to four. The remaining three are a different defect — keyword-only
parameters hidden behind defaulted positionals — and stay unbuilt.

Type of change

  • feat: A new feature
  • docs: Documentation only changes (if related to the new feature)
  • perf: A code change that improves performance (if part of the feature)
  • test: Adding missing tests or correcting existing tests (for the new feature)
  • chore: Changes to the build process or auxiliary tools and libraries suchs as documentation generation (if related to the new feature)
  • ci: Changes to our CI configuration files and scripts (if related to the new feature)
  • build: Changes that affect the build system or external dependencies (if related to the new feature)
  • revert: Reverts a previous commit

Breaking Change

  • Yes
  • No
    (If Yes, please describe the impact and migration path below):

N/A for the API surface: no function is removed and no public symbol is renamed. All 28
functions this PR adds to utils/expression.py are private (_-prefixed), and the diff removes
zero functions. Downstream repositories import six symbols from testsuite.utils —
select_fixture (45 uses), requires_protocol (4), requires_json_operations (2),
roundtrip_expression, skip_test_if_protocol_unsupported and assert_params_equal — and all six
are still present with unchanged signatures. requires_protocol's own behaviour is untouched; only
the strings passed to it changed.

The behaviour change is in coverage, not in the interface: tests that used to skip now run. A
backend carrying a latent defect can therefore start failing where it previously passed. The nine
backends that were pointed at this branch while it was in development are the evidence that it does
not — see Testing.

Related Repositories/Packages

This is the eleventh and last repository in the coordinated feature/expression-schema-object-refactor
change. All the others are merged:

Repository PR Merged into
rhosocial/python-activerecord #125 release/v1.0.0.dev30
rhosocial/python-activerecord-postgres #61 release/v1.0.0.dev17
rhosocial/python-activerecord-mysql #60 release/v1.0.0.dev20
rhosocial/python-activerecord-mariadb #9 release/v1.0.0.dev3
rhosocial/python-activerecord-oracle #9 release/v1.0.0.dev3
rhosocial/python-activerecord-sqlserver #7 release/v1.0.0.dev3
rhosocial/python-activerecord-snowflake #5 release/v1.0.0.dev2
rhosocial/python-activerecord-firebird #7 release/v1.0.0.dev2
rhosocial/python-activerecord-bigquery #3 release/v1.0.0.dev1
rhosocial/python-activerecord-clickhouse #4 release/v1.0.0.dev1

Dependency. This branch requires core at release/v1.0.0.dev30 or later — including
f84320f, a follow-up fix on that line that binds the Subquery that SQLValueExpression.in_()
builds (it was constructed with None as the dialect). Without it,
feature/query/joins/test_join_subquery.py::test_join_filtered_by_in_subquery fails wherever the
relation renders through that node; it was observed failing on BigQuery's emulator, and passed on
all backends once the core fix was on the line.

Testing

  • I have added new tests to cover my changes.
  • I have updated existing tests to reflect my changes.
  • All existing tests pass.
  • This change has been tested on [specify backend/DB versions].

Test Plan:

This repository has no CI pipeline — it is a shared test suite library installed into the other
projects' virtual environments, so it has no self-tests to run. Verification therefore has two
parts.

Locally, after the fix: the full-text-search compatibility test runs 11 passed (it skipped
11 of 11 before), and the join bridge files run 209 passed, with nothing newly failing.

Through the backends. While this branch was in development, all nine backends' test.yml pinned
the testsuite at feature/expression-schema-object-refactor, so this exact branch ran inside nine
full CI matrices against core release/v1.0.0.dev30 (including f84320f):

Backend Matrix versions Result
postgres 3.8–3.14 × PostgreSQL 9–19beta4 14 jobs pass; 2 jobs hit the 6-hour Actions limit (see below)
mysql 3.8–3.14 × MySQL 5.6–9.7 19 pass
mariadb 3.8–3.14 × MariaDB 10.2–13.1rc 19 pass
oracle 3.14 × Oracle 18c / 21c / 23c 8 pass
sqlserver 3.8–3.15rc × SQL Server 2017–2025 10 pass; 2 jobs hit the 6-hour Actions limit
snowflake 3.8–3.14 (render + docs only, no instance) 11 pass
firebird 3.8–3.14 × Firebird 5 / 6 6 pass
bigquery 3.10–3.14 (emulator, no instance) 9 pass
clickhouse 3.8–3.14 × ClickHouse 25.8 / 26.3 / 26.7 8 pass

"All existing tests pass" is deliberately left unticked. No test failed an assertion anywhere,
but four jobs did not report a pass, and I am not going to count a timeout as a pass:

  • postgres Python 3.14 / postgres 19beta4 and Run benchmarks with Python 3.14 / postgres 18 —
    both ran 6h0m42s / 6h0m43s and were cut off by the Actions job limit;
  • sqlserver Python 3.11 / SQL Server 2019 and Python 3.12 / SQL Server 2025 — 6h0m41s /
    6h1m59s
    , same cause;
  • both runs are reported as cancelled.

These are timeouts, not assertion failures, and they are not attributable to this branch: the same
two jobs passed in an earlier workflow_dispatch run of the same workflow, and the pattern appears
in the backend's pull_request runs both before and after this suite's fragment commit — so it
does not track this change. I could not retrieve their logs (the archive fetch returned truncated
output), so the hang itself is undiagnosed; it is worth a separate look at the service containers
for those images.

One further red is not a test result at all: SonarCloud Code Analysis fails on mysql, mariadb,
oracle, sqlserver and firebird because the dependency-repoint commit in those PRs touched the
pip install ... git+https://... lines in .github/workflows/test.yml, which SonarCloud's
githubactions:S8544 / S8541 rules then score as new-code security findings
(new_security_rating 3 against a threshold of 1). The same lines existed before under a different
ref; making them new code is what surfaced them. Those PRs were merged with the ruleset's
OrganizationAdmin bypass.

Checklist

  • My code follows the project's code style guidelines.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have made corresponding changes to the documentation (Docstrings, README.md, etc.).
  • My changes generate no new warnings.
  • I have added a Changelog fragment (changelog.d/<issue_number>.<type>.md).
  • I have verified that my changes do not introduce SQL injection vulnerabilities.
  • I have checked for potential performance regressions.

Two boxes are unticked on purpose. No changelog fragment yet — the fragment is numbered for the
PR, so it can only be written once the PR exists; it is added in a follow-up commit. No new
warnings / SQL injection / performance
: this change touches test fixtures and one test helper, not
the SQL-rendering path, and nothing here adds a query, a warning, or per-test work beyond
constructing the expressions the tests already needed. The docs claim is ticked only for the
docstring corrections in utils/common.py and utils/expression.py.

Additional Notes

The common thread is that a skip is not a failure. Both defects turned "I could not check this"
into a green run, and in the first case eleven parametrisations of a compatibility test plus every
join test across every backend reported success while testing nothing. requires_protocol treats an
unresolvable reference as "not supported" and skips; that is the right behaviour for a probe that
legitimately answers False, and the wrong behaviour for a reference that cannot be imported at
all. Nothing here changes that policy — but it is why the two are worth telling apart, and why a
green matrix is not by itself evidence that a suite is running.

Worth noting for whoever picks this up next: this branch's real function is as the last piece of
the eleven-repository change, and the fact that it is also the piece whose failures were silent is
not a coincidence. The two adapters — a resolver that skips, and a constructor that guesses — were
both written to be forgiving, and both were wrong in the same direction.

Core's expression classes now take the object they act on rather than a parsed string,
so the fixtures construct them from objects and several need explicit constructors
where the introspective guess cannot satisfy the shape.

The introspective constructor read a parameter's annotation and computed its text as
the alias' own __name__. If that text mentioned a relation word -- Table, View,
Sequence and the rest -- it handed the parameter a catalogue object. List[str] was
safe because the container branch answered first, but Sequence[X] was not:
get_origin yields collections.abc.Sequence, the test for list/tuple/set misses, and
what is left is the literal string "Sequence". So a parameter asking for a list of
columns received a Sequence object and the class could not be built.

_empty_container_for now answers for any parameterised alias before the relation-name
test, matching the collections.abc ABCs rather than the concrete types an annotation
happens to name, so Sequence[str], FrozenSet[str] and Mapping[str, int] are recognised
as what they are. A bare class annotation returns None, so the relation test still
sees the parameters that really do name an object.

Measured by walking every class's __init__ in the core expression package and
re-deriving what the old code decided: 58 parameters reached the relation test, 51 of
them annotated with a single class, and 7 wrongly annotated with a parameterised
alias -- columns, attributes, actions, parts, items, keys and passing, all
Sequence[...]. Against MariaDB the seven classes went from none constructible to four.
The remaining three are a different defect, keyword-only parameters hidden behind
defaulted positionals, and stay unbuilt.
… tests run

`requires_protocol` resolves a string with `rsplit('.', 1)` before importing it. Two
forms in this suite cannot resolve, and both make the test SKIP rather than fail:

* the bare name `"JoinSupport"` and `"SchemaSupport"` -- no dot, so `rsplit` raises
  ValueError and the resolver skips;
* the dotted path `...dialect.protocols.IndexSupport`, which stopped resolving when
  the core's single `protocols.py` became a `protocols/` package and the class was
  renamed.

The effect was invisible: the full-text-search compatibility test skipped 11 of 11
parametrisations and every join test skipped, on every backend, with no failure to
show for it. Verified after the fix -- full-text now runs 11 passed, and the join
bridge files run 209 passed, with nothing newly failing.
@vistart vistart added the enhancement New feature or request label Oct 8, 2026
@vistart vistart self-assigned this Oct 8, 2026
@sonarqubecloud

sonarqubecloud Bot commented Oct 8, 2026

Copy link
Copy Markdown

@vistart
vistart merged commit ff3cef1 into release/v1.0.0.dev17 Oct 8, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant