Skip to content

Fix GeometryField value conversion crashing on non-None values - #2238

Open
binggao1230 wants to merge 5 commits into
tortoise:developfrom
binggao1230:fix-geometryfield-field-type
Open

binggao1230 wants to merge 5 commits into
tortoise:developfrom
binggao1230:fix-geometryfield-field-type

Conversation

@binggao1230

@binggao1230 binggao1230 commented Jul 7, 2026 •

Copy link
Copy Markdown

Description

GeometryField (MySQL) subclasses Field directly with a single base:

class GeometryField(Field):
    SQL_TYPE = "GEOMETRY"

The _FieldMeta metaclass only auto-assigns field_type when a field has
multiple bases (len(bases) > 1 and bases[0] is Field). A single-base
subclass therefore keeps the class default field_type = None.

The base value converters use it directly:

def to_db_value(self, value, instance):
    ...
    if isinstance(value, self.field_type):   # isinstance(value, None)

So any non-None value round-tripping through GeometryField raises:

TypeError: isinstance() arg 2 must be a type, a tuple of types, or a union

This trips both to_python_value (reading a geometry back from the DB) and
to_db_value (writing one).

Fix

Declare a concrete field_type and parametrize the generic, exactly as
#2237 did for
TSVectorField:

class GeometryField(Field[str]):
    SQL_TYPE = "GEOMETRY"
    field_type = str

Tests

Added tests/contrib/mysql/test_geometry_field.py (DB-free) asserting
field_type is str and that to_python_value/to_db_value handle both
None and a concrete "POINT(1 1)" value without raising. Reproduces as a
TypeError before the fix; passes after.

tests/fields/, tests/schema/test_generate_schema.py and the new test all
pass (474 passed, 66 skipped); ruff format --check and ruff check clean.

Maintainer follow-up

The direct-Field census on current develop found six classes. TSVectorField, CharField, BooleanField, and TimeDeltaField already declare concrete field_type values; GeometryField was the only concrete omission. RelationalField is the intentional non-column base (has_db_field = False), and its concrete relation instances receive their type during app initialization. I therefore kept this PR scoped to GeometryField rather than add the optional broad meta-test.

The instance=None calls in the unit test deliberately use targeted # type: ignore[arg-type] comments because GeometryField.to_db_value does not inspect instance. Widening the base method signature would be a separate API/type change. The field now also documents that MySQL GEOMETRY is exposed as its WKT string representation.

GeometryField subclasses Field directly (single base), so the _FieldMeta
metaclass — which only auto-assigns field_type when there are multiple
bases — leaves field_type as None. The base to_python_value/to_db_value
converters then call isinstance(value, self.field_type), raising
TypeError: isinstance() arg 2 must be a type ... on any non-None value.

Declare field_type = str and parametrize as Field[str], mirroring the
fix applied to TSVectorField in tortoise#2237.
@codspeed

codspeed Bot commented Jul 11, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 24 untouched benchmarks


Comparing binggao1230:fix-geometryfield-field-type (7a1ffae) with develop (5815573)

Open in CodSpeed

binggao1230 and others added 2 commits July 11, 2026 16:42
The `instance` parameter is typed `type[Model] | Model`; passing `None`
in these unit tests is fine at runtime (GeometryField's converter does not
use it) but trips mypy, so annotate the two calls as the codebase does
elsewhere.
@waketzheng

Copy link
Copy Markdown
Contributor

The fix is correct and consistent with #2237. A few things worth addressing while it's open:

1. Are there other single-base Field subclasses with the same issue?

_FieldMeta only auto-assigns field_type when len(bases) > 1 and bases[0] is Field, so any field class that subclasses Field directly without declaring field_type has the same latent bug. GeometryField and TSVectorField (#2237) are two known cases — could you check whether there are others (grep "class .*Field(Field")? If so, either fix them here or list them for a follow-up. That would keep this class of bug from resurfacing as a new issue.

2. GeometryField could use a docstring.

Something like:

class GeometryField(Field[str]):
    """MySQL GEOMETRY column, exposed as its WKT string representation."""
    SQL_TYPE = "GEOMETRY"
    field_type = str

This documents the MySQL-only constraint and explains why field_type = str — the Python-side representation really is a string, not a geometry object.

3. The instance=None in the to_db_value tests.

The # type: ignore[arg-type] is explained, but a cleaner fix would be to widen the base to_db_value signature to instance: Model | None. That's a bigger change than this PR, so the current approach is fine — just worth noting in the PR description that it's a deliberate workaround.

4. Optional: a meta-level regression test.

A lightweight test that walks the built-in field classes and asserts each has a non-None field_type (with an explicit allowlist if some fields legitimately allow None) would catch this class of bug at the source rather than case by case. Optional, and lower priority than the points above — skip it if it feels like over-engineering for this PR.

Nothing here blocks the fix itself.

@binggao1230

Copy link
Copy Markdown
Author

Thanks — I checked the current develop tree. The direct subclasses are GeometryField, TSVectorField, CharField, BooleanField, TimeDeltaField, and RelationalField. The middle four already declare concrete types; GeometryField was the only concrete omission. RelationalField is the intentional non-column base (has_db_field = False), and its concrete relation fields get their type during app initialization.

I added the suggested docstring in 7a1ffae and documented both the census and the deliberate instance=None type-ignore workaround in the PR description. I left out the optional meta-test because it would need an exception for the relational base without covering another concrete defect.

Validation: the focused tests pass (3/3), ruff and mypy pass on the touched files, and the full SQLite-backed suite passes (1,913 passed, 148 skipped, 2 xfailed).

This branch has not been deployed

No deployments
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