Skip to content

fix: correct isinstance and issubclass results in compiled builds - #661

Merged
cofin merged 1 commit into
mainfrom
fix/mypyc-compiled-runtime-casts
Jul 27, 2026
Merged

fix: correct isinstance and issubclass results in compiled builds#661
cofin merged 1 commit into
mainfrom
fix/mypyc-compiled-runtime-casts

Conversation

@cofin

@cofin cofin commented Jul 27, 2026

Copy link
Copy Markdown
Member

Compiled builds returned wrong answers for isinstance and issubclass on SQLSpec's own class hierarchies, and which answer you got depended on which check happened first in the process. An ordinary type check was enough to break query execution.

isinstance(select, CreateIndex)   # False - an ordinary, correct check
isinstance(select, QueryBuilder)  # False - WRONG, poisoned by the line above
session.execute(select)           # AttributeError: 'Select' object has no attribute 'sql'
  • Cause. mypyc builds compiled classes without running ABCMeta.__new__ for each class, so every class in a hierarchy inherited one shared cache object. The first isinstance/issubclass result was written into that shared cache and then returned for every later check against any class in the hierarchy. All 54 abstract classes across the builder, core, migrations, storage, and serializer modules shared a single cache.
  • Fix. Abstract bases now use plain inheritance instead of ABC. This removes the shared cache entirely — type checks become ordinary MRO lookups.
  • Enforcement is unchanged. abstractmethod is retained, so mypy still reports Cannot instantiate abstract class and the type-check gate keeps enforcing implementations.
  • No runtime guard was lost. mypyc had already disabled it: with ABC in place, QueryBuilder() constructed successfully and __abstractmethods__ was absent. Nothing in the codebase uses virtual subclass registration.
  • Results stay iterable. StatementResult no longer inherits collections.abc.Iterable, which carries the same metaclass. It defines __iter__, so isinstance(result, Iterable) still succeeds through the structural check.
  • Also fixes a compiled-only crash in the migration runner, where a cast to a non-optional type raised TypeError: dict object expected; got None whenever a runner had no configuration attached. cast is a runtime type check under mypyc and the value is legitimately None.

Scope

The defect reproduces in a twelve-line file with no SQLSpec code involved, on CPython 3.10, 3.11, 3.13 and 3.14, and is not fixed upstream. @mypyc_attr(native_class=False) — the documented escape hatch — is not usable here: it crashes the compiler on classes with rich comparison methods, and emits invalid C for a non-native base with native subclasses.

Interpreted behavior is unaffected; the full interpreted suite passes. Compiled builds go from 54 shared-cache classes to zero.

Base automatically changed from fix/third-party-extension-migrations to main July 27, 2026 20:26
Compiled builds returned wrong answers for isinstance and issubclass on
SQLSpec's class hierarchies, and the answer depended on which check ran
first in the process. An ordinary type check was enough to break query
execution:

    isinstance(select, CreateIndex)   # False, an ordinary check
    isinstance(select, QueryBuilder)  # False -- wrong, poisoned by the above
    session.execute(select)           # AttributeError: 'Select' object has
                                      # no attribute 'sql'

mypyc builds compiled classes without running ABCMeta.__new__ for each
class, so every class in a hierarchy inherited a single shared cache
object. The first isinstance or issubclass result was written into that
shared cache and returned for every later check against any class in the
hierarchy. All 54 abstract classes across builder, core, migrations,
storage, and serializer modules shared one cache.

Abstract bases now use plain inheritance. abstractmethod is retained, so
mypy still reports "Cannot instantiate abstract class" and the type-check
gate keeps enforcing implementations. The runtime instantiation guard is
unaffected in practice because mypyc had already disabled it: with ABC in
place, QueryBuilder() constructed successfully and __abstractmethods__ was
absent. Nothing in the codebase relies on virtual subclass registration.

StatementResult no longer inherits collections.abc.Iterable, which carries
the same metaclass. It defines __iter__, so isinstance(result, Iterable)
still succeeds through the structural check.

Also corrects a cast in the migration runner that raised TypeError in
compiled builds whenever a runner had no configuration attached. cast is a
runtime type check under mypyc, and the value is legitimately None.

Reproduces on CPython 3.10 through 3.14 and is not fixed upstream.

Claude-Session: https://claude.ai/code/session_01CKBtk2CVvc3nNRAx4NJ56K
@codecov-commenter

codecov-commenter commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 77.22%. Comparing base (611d367) to head (03b22b0).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #661      +/-   ##
==========================================
+ Coverage   77.20%   77.22%   +0.01%     
==========================================
  Files         475      475              
  Lines       67825    67824       -1     
  Branches     9322     9322              
==========================================
+ Hits        52367    52375       +8     
+ Misses      12046    12037       -9     
  Partials     3412     3412              
Flag Coverage Δ
integration 60.78% <100.00%> (+<0.01%) ⬆️
py3.10 75.52% <100.00%> (+<0.01%) ⬆️
py3.11 75.53% <100.00%> (-0.01%) ⬇️
py3.12 75.54% <100.00%> (+<0.01%) ⬆️
py3.13 75.54% <100.00%> (+<0.01%) ⬆️
py3.14 76.42% <100.00%> (+0.01%) ⬆️
unit 64.73% <100.00%> (-0.01%) ⬇️

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

Files with missing lines Coverage Δ
sqlspec/builder/_base.py 75.94% <100.00%> (ø)
sqlspec/core/filters.py 91.68% <100.00%> (ø)
sqlspec/core/result/_base.py 84.85% <100.00%> (-0.04%) ⬇️
sqlspec/core/splitter.py 80.47% <100.00%> (ø)
sqlspec/migrations/base.py 93.36% <100.00%> (ø)
sqlspec/migrations/loaders.py 67.97% <100.00%> (ø)
sqlspec/migrations/runner.py 85.26% <100.00%> (ø)
sqlspec/storage/backends/base.py 95.12% <100.00%> (ø)
sqlspec/utils/serializers/_json.py 74.91% <100.00%> (ø)

... and 3 files with indirect coverage changes

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

@cofin
cofin force-pushed the fix/mypyc-compiled-runtime-casts branch from 7d18557 to 03b22b0 Compare July 27, 2026 20:33
@cofin
cofin merged commit ebcb937 into main Jul 27, 2026
24 checks passed
@cofin
cofin deleted the fix/mypyc-compiled-runtime-casts branch July 27, 2026 20:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants