Skip to content

Add spatial types - #7096

Open
ddegasperi wants to merge 7 commits into
doctrine:4.5.xfrom
ddegasperi:add-spatial-types
Open

Add spatial types#7096
ddegasperi wants to merge 7 commits into
doctrine:4.5.xfrom
ddegasperi:add-spatial-types

Conversation

@ddegasperi

@ddegasperi ddegasperi commented Aug 18, 2025

Copy link
Copy Markdown
Q A
Type feature
Fixed issues #7081

Summary

This PR introduces spatial data type support for DBAL, starting with PostgreSQL/PostGIS and MySQL. This implementation is intended to serve as a reference and guide for adding support for other database platforms in the future.

Core Spatial Types

  • GeometryType (Types::GEOMETRY) - For planar coordinate systems
  • GeographyType (Types::GEOGRAPHY) - For spherical earth coordinates
  • Both types handle GeoJSON format for data exchange

Schema API Integration

  • Extended Column and ColumnEditor with geometryType and srid properties
  • Clean fluent API: Column::editor()->setGeometryType('POINT')->setSrid(4326)
  • Full PostgreSQL platform support with PostGIS-specific SQL generation
  • Schema introspection and table creation/modification support

What's next

  • Discuss the PR
  • Add other platforms (PostgreSQL/PostGIS, MySQL)
  • Add spatial index (PostgreSQL/PostGIS, MySQL already support creation of spatial indexes)

Comment thread tests/Platforms/PostgreSQLPlatformTest.php
@greg0ire
greg0ire marked this pull request as draft August 26, 2025 14:16
@ddegasperi
ddegasperi force-pushed the add-spatial-types branch 3 times, most recently from 810f526 to 86b4b15 Compare August 28, 2025 06:44
Comment thread src/Types/GeometryType.php
@tibobaldwin

tibobaldwin commented Sep 6, 2025

Copy link
Copy Markdown

Adding spatial index management would be interesting for this PR I think :).

For instance:
CREATE INDEX mytable_geom_x ON mytable USING GIST (geom)

Sources:

@ddegasperi

Copy link
Copy Markdown
Author

Adding spatial index management would be interesting

Thanks for the advice — I agree, index management is definitely an important aspect when working with geometry data. I haven’t investigated it in depth yet, but it looks like this might already be supported by specifying the index type. A quick search in the repository shows an IndexType enum with a SPATIAL option, so I’ll take a closer look at how that could be integrated here.

@ddegasperi
ddegasperi force-pushed the add-spatial-types branch 2 times, most recently from ba2a7aa to a33fcd8 Compare October 10, 2025 13:06
@ddegasperi
ddegasperi force-pushed the add-spatial-types branch 5 times, most recently from 0b81371 to a3544b1 Compare October 14, 2025 15:24
@ddegasperi ddegasperi changed the title [DRAFT] Add spatial types Add spatial types Oct 14, 2025
@ddegasperi
ddegasperi marked this pull request as ready for review October 14, 2025 15:25
@jungleman12

Copy link
Copy Markdown

Wow, that's exactly what I'd like to see integrated into Doctrine soon. Great work!

@derrabus
derrabus changed the base branch from 4.4.x to 4.5.x November 29, 2025 11:31
@ddegasperi
ddegasperi force-pushed the add-spatial-types branch 2 times, most recently from 98c3be5 to e9e86f4 Compare December 5, 2025 14:27
@ddegasperi

Copy link
Copy Markdown
Author

Hi @derrabus

I’ve addressed all the feedback received so far and updated the PR accordingly.
When you get a chance, could you please let me know if there’s anything else I can do to help move this forward?

Thanks a lot for your time

@derrabus derrabus left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank your for all the work you've put into this PR. Your changes look very promising.

Comment thread .github/workflows/continuous-integration.yml Outdated
Comment thread .github/workflows/phpunit-postgis.yml Outdated
Comment thread src/Platforms/AbstractMySQLPlatform.php Outdated
Comment thread src/Schema/MySQLSchemaManager.php Outdated
Comment thread src/Types/GeoJSON.php Outdated
Comment thread src/Connection.php
Comment on lines +547 to +556
/**
* Normalizes types array from positional or associative to associative format.
*
* @param array<int<0,max>, string|ParameterType|Type>|array<string, string|ParameterType|Type> $types
* @param list<string> $columnNames
*
* @return array<string, string|ParameterType|Type>
*/
private function normalizeTypes(array $types, array $columnNames): array
{

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we have to do this?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I introduced normalizeTypes() because I needed a reliable way to support SQL-level type conversion via Type::convertToDatabaseValueSQL(), which is required for spatial types and may also be relevant for other custom types in the future.

With insert() and update(), the $types argument can be passed in two different forms:

  • Positional, e.g. [0 => 'geometry', 1 => 'string']
  • Associative, e.g. ['location' => 'geometry', 'name' => 'string']

The new getPlaceholderForColumn() method operates at the column level and needs to know whether a specific column requires SQL-level conversion using convertToDatabaseValueSQL(). For that decision, the type must be known by column name.

When $types is positional, there is no straightforward or safe way to determine the type for a given column name at that point. I added normalizeTypes() to normalize both positional and associative $types into a single, column-name–keyed structure. This allows getPlaceholderForColumn() to consistently determine:

  • which type applies to each column
  • whether SQL-level conversion should be applied

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the current type abstraction does not work for these new types? It's quite unusual that we need to change the connection class on the wrapper layer for introducing new types.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the discussion — I did a bit more digging to better understand how type conversion is currently applied.

In DBAL itself, convertToDatabaseValue() is called when binding parameters, so value-level conversion works as expected. However, I couldn’t find any code paths in DBAL where convertToDatabaseValueSQL() (or convertToPHPValueSQL()) is actually invoked. As far as I can tell, these methods are currently required and used by doctrine/orm, not by DBAL directly.

This becomes relevant for spatial types because converting GeoJSON into a native geometry value in PostGIS, MySQL, or MariaDB cannot be done via casting — all of them require an explicit SQL function call such as ST_GeomFromGeoJSON(...). If the SQL-level conversion hook isn’t invoked, any custom SQL logic implemented in convertToDatabaseValueSQL() would effectively be ignored.

From this perspective, DBAL currently:

  • supports value-level conversion at bind time (convertToDatabaseValue()),
  • but does not apply SQL-level conversion hooks during insert() / update() flows.

That’s the context in which I explored a possible solution at the connection layer: without a place where convertToDatabaseValueSQL() is actually used, this appeared to be a way to express database-specific SQL transformations while still allowing users to work with a user-friendly input format like GeoJSON.

Happy to adjust the approach if there’s a more idiomatic way to support this in DBAL — I mainly wanted to share these findings to provide some additional context around the motivation.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So there's no binary format that can be insterted directly into those columns? I've run into similar problems while working on the VECTOR type. And you're right, convertToDatabaseValueSQL() is never called by the DBAL, currently. Maybe we need a better abstraction here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question — this does indeed seem very similar to the VECTOR case.

While geometry columns can sometimes accept WKB directly, conversion functions like ST_GeomFromWKB() or ST_GeomFromGeoJSON() are generally the more robust and recommended approach across spatial databases, as they make the conversion explicit and behave consistently.

There are also some portability concerns with a pure WKB-based approach:

  • EWKB (with embedded SRID) is a PostGIS extension and isn’t supported by MySQL or MariaDB, so SRID handling would still require additional logic.
  • Relying on implicit casting of binary values feels more fragile than using explicit SQL functions, especially across different platforms and versions.

Because of that, a WKB-based workaround could work in some cases, but it feels more like a workaround than a solid abstraction.

Given this, I’d appreciate some guidance on direction:

  • should we explore a WKB-based value-level approach despite these trade-offs, or
  • is it worth discussing a DBAL-level abstraction for types that require SQL-level transformation during insert/update (which would also apply to cases like VECTOR)?

Personally, I would lean towards exploring a proper abstraction if that aligns with DBAL’s design goals, but I’m happy to follow the direction you think makes the most sense.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it worth discussing a DBAL-level abstraction for types that require SQL-level transformation during insert/update

I think so, yes.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before going deeper into an implementation, I’d like to better understand what kind of abstraction you have in mind. From my side, I was trying to reason about where SQL-level type conversion could live in a way that stays consistent and avoids duplication.

One idea I explored was whether it would make sense for Connection::insert() / update() to internally rely on QueryBuilder, so that SQL-level conversion (via convertToDatabaseValueSQL()) could be handled in a single place rather than re-implemented in multiple code paths.

That said, I’m not attached to this approach — I’d be very interested to hear your thoughts on:

  • what shape you imagine this abstraction taking, and
  • whether using QueryBuilder inside Connection would align with DBAL’s architectural direction, or if you’d prefer to keep them clearly independent.

Comment thread tests/Functional/Schema/MySQL/SpatialTypesTest.php Outdated
Comment thread tests/Functional/Schema/MySQL/SpatialTypesTest.php Outdated
Comment thread tests/Functional/Schema/PostgreSQL/PostGISTest.php Outdated
Comment thread tests/TestUtil.php Outdated
@ddegasperi

Copy link
Copy Markdown
Author

Thank your for all the work you've put into this PR. Your changes look very promising.

Thanks a lot for your review and the detailed feedback!
I really appreciate the time you took to go through the PR.
I’ll work on the requested changes as soon as possible and hopefully be able to address everything within the next week.

Comment thread tests/SpatialReferenceSystems.php Outdated
@derrabus

derrabus commented Jan 2, 2026

Copy link
Copy Markdown
Member

the test suite passes on both MySQL 5.7 and MySQL ≥ 8

No, it still fails on 5.7.

@ddegasperi
ddegasperi force-pushed the add-spatial-types branch 3 times, most recently from b702189 to 84f01c9 Compare January 14, 2026 07:25
@ddegasperi

Copy link
Copy Markdown
Author

Just a gentle follow-up on this topic. I completely understand things get busy — I just wanted to check whether you’ve had a chance to think about the abstraction question around SQL-level type conversion.

I’m happy to move forward in whichever direction you think fits best (e.g. exploring a QueryBuilder-based approach, or something different). If you have a preference, that would help me focus the implementation accordingly.

@seb-jean

Copy link
Copy Markdown

Hi @ddegasperi, there is still an error related to PHPStan in the pipeline, but I am unsure if it is related to your PR.

@ddegasperi

Copy link
Copy Markdown
Author

Hi @ddegasperi, there is still an error related to PHPStan in the pipeline, but I am unsure if it is related to your PR.

@seb-jean The reported PHPStan error points to src/Schema/ColumnDiff.php, which is not touched by this PR. The issue appears to be triggered by the introduction of the PHP 8.5 checks and was already addressed in the base branch by @derrabus (via an update to the PHPStan baseline).

I’ve rebased my branch on the latest 4.5.x branch including that fix, so the pipeline should now pass on the next run.

@seb-jean

Copy link
Copy Markdown

That's exactly what I thought when I went through PR's code.
Thank you for your quick response and explanation.

Comment thread src/Platforms/PostgreSQLPlatform.php Outdated
Comment thread src/Types/Geometry.php
*
* @throws InvalidArgumentException If the GeoJSON format is invalid.
*/
public static function fromGeoJSON(string $json): self

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I need to do something like this to add a Point type data point:

$location->setCoordinates(Geometry::fromGeoJSON((string) json_encode([
   'type' => 'Point',
   'coordinates' => [$location->getLongitude(), $location->getLatitude()],
   'crs' => ['type' => 'name', 'properties' => ['name' => 'EPSG:4326']],
])));

I'm finding the average DX, but I don't think there's another way to do it?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point — I agree the current DX isn't ideal. Having to hand-build the array, json_encode it, and spell out the full crs block just to store a single Point is more ceremony than it should be.

As a first, lightweight option, I was thinking of adding named constructors on the Geometry value object covering the RFC 7946 types (a sketch is at the bottom of this comment). They would all funnel through the existing fromGeoJSON/toGeoJSON internals, so the wire format stays GeoJSON and the round-trip keeps working through a single bind parameter (ST_GeomFromGeoJSON(?), with the SRID carried inside the payload). The factories would purely be an ergonomic front door — nothing changes underneath.

I deliberately leaned away from a full typed hierarchy (Point, LineString, … as separate classes). It would be the richer model, but a complete geometry type system — coordinate validation, Z/M dimensions, accessors, equality — feels more like the domain of dedicated third-party geometry libraries, and I'd be cautious about DBAL reinventing and then having to maintain that surface long-term.

Before going further, it would really help to hear from your side:

  • What does your data usually look like — raw coordinates like the sketch below, or do you already have WKT/EWKT strings on hand? The former fits these factories nicely; the latter would point more towards a PHP-side fromWKT() that parses into the same GeoJSON-backed object rather than touching the wire format.
  • Would constructors like these actually smooth out your workflow, or is there something else in the day-to-day usage that feels awkward?

This is just an early idea, and the final shape of the public API would of course be up to the maintainers — but I'd like to make sure whatever we end up proposing genuinely solves the use case you ran into.


For reference, here's how the constructors might look:

// Point: longitude, latitude
Geometry::point(11.34, 46.49, srid: 4326);

// LineString: a list of [lon, lat] positions
Geometry::lineString([
    [11.34, 46.49],
    [11.36, 46.50],
    [11.38, 46.48],
], srid: 4326);

// Polygon: a list of linear rings — the first is the exterior ring,
// any further rings are holes. Each ring is closed (first position === last).
Geometry::polygon([
    [[11.0, 46.0], [11.5, 46.0], [11.5, 46.5], [11.0, 46.5], [11.0, 46.0]], // exterior
    [[11.1, 46.1], [11.2, 46.1], [11.2, 46.2], [11.1, 46.1]],               // hole (optional, repeatable)
], srid: 4326);

// MultiPoint: a list of points
Geometry::multiPoint([
    [11.34, 46.49],
    [11.40, 46.51],
], srid: 4326);

// MultiLineString: a list of LineStrings
Geometry::multiLineString([
    [[11.34, 46.49], [11.36, 46.50]],
    [[11.40, 46.51], [11.42, 46.52]],
], srid: 4326);

// MultiPolygon: a list of Polygons (each Polygon being a list of rings:
// exterior ring first, optional holes after)
Geometry::multiPolygon([
    [
        [[11.0, 46.0], [11.5, 46.0], [11.5, 46.5], [11.0, 46.5], [11.0, 46.0]], // polygon 1 — exterior
        [[11.1, 46.1], [11.2, 46.1], [11.2, 46.2], [11.1, 46.1]],               // polygon 1 — hole
    ],
    [
        [[12.0, 47.0], [12.5, 47.0], [12.5, 47.5], [12.0, 47.0]],               // polygon 2 — exterior only
    ],
], srid: 4326);

// GeometryCollection: a list of Geometry objects
Geometry::collection([
    Geometry::point(11.34, 46.49),
    Geometry::lineString([[11.34, 46.49], [11.36, 46.50]]),
], srid: 4326);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For me, it's simple: I have the latitude and longitude of different cities, for example, and I want to insert them into a database.

Comment thread .github/workflows/continuous-integration.yml Outdated
Comment thread src/Platforms/MariaDBPlatform.php Outdated
Comment thread src/Platforms/PostgreSQLPlatform.php
Comment thread src/Platforms/PostgreSQLPlatform.php
@seb-jean

seb-jean commented May 12, 2026

Copy link
Copy Markdown

@ddegasperi, I was finally able to test this PR on a real PostGIS instance and it works perfectly. One important thing is still missing: documentation for this new feature.

@seb-jean

Copy link
Copy Markdown

Once the PR is merged, we may be able to add functions in src/Query/AST/Functions of the doctrine/orm project.

@ddegasperi

Copy link
Copy Markdown
Author

@seb-jean Thank you for taking the time to test the PR and for feedback - I really appreciate it.

You are absolutely right about the missing documentation, and the suggestions regarding potential additions in Doctrine ORM also make sense. I’ll also go through the remaining code suggestions and address them accordingly.

At the moment, I still need to finish some urgent work for a project over the next two weeks, but after that I plan to come back to this PR and work through the open points.

Thanks again for the review and the valuable feedback.

@ddegasperi

Copy link
Copy Markdown
Author

@seb-jean I've finally had time to come back to this PR and work through the open points.

I've now applied most of the suggestions from your review, and I believe the remaining feedback is addressed as well. There are still a couple of points around developer experience (e.g. how geometry values are constructed) that I've left open for discussion, since I'd like to settle on the right direction together before implementing them.

Whenever you get a chance, I'd really appreciate it if you could take another look.

@seb-jean

Copy link
Copy Markdown

@seb-jean I've finally had time to come back to this PR and work through the open points.

I've now applied most of the suggestions from your review, and I believe the remaining feedback is addressed as well. There are still a couple of points around developer experience (e.g. how geometry values are constructed) that I've left open for discussion, since I'd like to settle on the right direction together before implementing them.

Whenever you get a chance, I'd really appreciate it if you could take another look.

I find the DX regarding the use of Geometry:: interesting. Thank you :)

Another thing, I haven't seen the documentation.

@ddegasperi

Copy link
Copy Markdown
Author

@seb-jean Thanks for the reminder about the documentation — that was a missing piece.

I've now added it. There's a dedicated reference page at docs/en/reference/spatial-types.rst (linked from the sidebar), plus a spatial section in docs/en/reference/types.rst. It covers the geometry/geography types, the per-platform support matrix (PostGIS vs. MySQL/MariaDB), declaring spatial columns, spatial indexes, and reading/writing values through the Geometry value object.

On the DX side — your lat/lon-of-cities use case is exactly the one the named constructors are meant to smooth out: Geometry::point(11.34, 46.49, srid: 4326) instead of hand-building the GeoJSON + crs block. They'd be a pure ergonomic front door funnelling through the existing fromGeoJSON/toGeoJSON internals, so the GeoJSON wire format and the single-bind-parameter round-trip stay untouched.

@derrabus — before I implement this, I'd really value your guidance on the direction:

  • Are you comfortable with lightweight named constructors on the Geometry value object (Geometry::point(), lineString(), polygon(), …), all backed by GeoJSON?
  • I've deliberately stayed away from a full typed hierarchy (Point, LineString, … as separate classes with coordinate/Z-M validation, accessors, equality) — that feels closer to the domain of dedicated geometry libraries, and I'd rather not have DBAL take on that maintenance surface. Does that boundary match how you'd want to scope it?

Add geometry and geography types for spatial data

Introduces new spatial data types for handling geometric and geographic data in database applications. GeometryType handles planar coordinates while GeographyType handles spherical earth coordinates, both using GeoJSON format.

This provides foundation for spatial data operations across database platforms that support spatial extensions.
Extends Column and ColumnEditor with geometryType and srid properties to support PostgreSQL's PostGIS spatial types and provides a clean schema API for working with GEOMETRY and GEOGRAPHY columns while maintaining backward compatibility.

This builds on the core spatial types implementation to complete the PostgreSQL spatial type support at the schema level.
This commit adds functional testing for PostGIS spatial types (GEOMETRY and GEOGRAPHY) with schema introspection and CI integration.

The implementation leverages PostgreSQL's native type system for introspection, making it compatible with any PostgreSQL instance without requiring PostGIS system tables to be accessible during schema operations.
Extends AbstractMySQLPlatform with GEOMETRY type support and enhances MySQLSchemaManager to introspect spatial columns with geometryType and SRID properties.

MySQL supports GEOMETRY types (POINT, LINESTRING, POLYGON, etc.) with optional SRID constraints using the conditional comment syntax for MySQL 8.0.3+.
Refactor GeometryType and GeographyType to replace direct GeoJSON string handling with dedicated value objects, preventing exposure of database-specific formats to application code.

This commit introduces a Geometry value object that encapsulates a supporting GeoJSON value object responsible for validating and wrapping GeoJSON representations.
Implement spatial index creation and introspection for PostgreSQL using the GIST (Generalized Search Tree) index method — the standard access method for spatial data in PostGIS.
This commit introduces a new getIndexMethodSQL() hook in AbstractPlatform for platform-specific index clauses, and overrides it in PostgreSQLPlatform to emit "USING GIST" for spatial indexes.

SQL generation examples:
MySQL → CREATE SPATIAL INDEX idx ON table (col)
PostgreSQL → CREATE INDEX idx ON table USING GIST (col)
Spatial types need more than a standard type entry because they
introduce the Geometry/GeoJSON value objects. The bulk goes in a new
reference/spatial-types.rst guide, with concise geometry and geography
entries in reference/types.rst cross-linking to it.
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.

5 participants